支持添加删除服务器信息,以在服务器状态页显示CPU、内存等
This commit is contained in:
3
lib/core/extension/stringx.dart
Normal file
3
lib/core/extension/stringx.dart
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
extension StringX on String {
|
||||||
|
int get i => int.parse(this);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:toolbox/core/persistant_store.dart';
|
||||||
|
import 'package:toolbox/view/widget/card_dialog.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
void unawaited(Future<void> future) {}
|
void unawaited(Future<void> future) {}
|
||||||
@@ -24,19 +26,44 @@ void showSnackBarWithAction(
|
|||||||
|
|
||||||
Future<bool> openUrl(String url) async {
|
Future<bool> openUrl(String url) async {
|
||||||
print('openUrl $url');
|
print('openUrl $url');
|
||||||
|
|
||||||
if (!await canLaunch(url)) {
|
if (!await canLaunch(url)) {
|
||||||
print('canLaunch false');
|
print('canLaunch false');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ok = await launch(url, forceSafariVC: false);
|
final ok = await launch(url, forceSafariVC: false);
|
||||||
|
|
||||||
if (ok == true) {
|
if (ok == true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('launch $url failed');
|
print('launch $url failed');
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<T?>? showRoundDialog<T>(
|
||||||
|
BuildContext context, String title, Widget child, List<Widget> actions,
|
||||||
|
{EdgeInsets? padding}) {
|
||||||
|
return showDialog<T>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) {
|
||||||
|
return CardDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: child,
|
||||||
|
actions: actions,
|
||||||
|
padding: padding,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildSwitch(BuildContext context, StoreProperty<bool> prop,
|
||||||
|
{Function(bool)? func}) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: prop.listenable(),
|
||||||
|
builder: (context, bool value, widget) {
|
||||||
|
return Switch(
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (func != null) func(value);
|
||||||
|
prop.put(value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
46
lib/data/model/disk_info.dart
Normal file
46
lib/data/model/disk_info.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
class DiskInfo {
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"mountPath": "",
|
||||||
|
"mountLocation": "",
|
||||||
|
"usedPercent": 0,
|
||||||
|
"used": "",=
|
||||||
|
"size": "",
|
||||||
|
"avail": ""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
String? mountPath;
|
||||||
|
String? mountLocation;
|
||||||
|
double? usedPercent;
|
||||||
|
String? used;
|
||||||
|
String? size;
|
||||||
|
String? avail;
|
||||||
|
|
||||||
|
DiskInfo({
|
||||||
|
this.mountPath,
|
||||||
|
this.mountLocation,
|
||||||
|
this.usedPercent,
|
||||||
|
this.used,
|
||||||
|
this.size,
|
||||||
|
this.avail,
|
||||||
|
});
|
||||||
|
DiskInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
mountPath = json["mountPath"]?.toString();
|
||||||
|
mountLocation = json["mountLocation"]?.toString();
|
||||||
|
usedPercent = double.parse(json["usedPercent"]);
|
||||||
|
used = json["used"]?.toString();
|
||||||
|
size = json["size"]?.toString();
|
||||||
|
avail = json["avail"]?.toString();
|
||||||
|
}
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data["mountPath"] = mountPath;
|
||||||
|
data["mountLocation"] = mountLocation;
|
||||||
|
data["usedPercent"] = usedPercent;
|
||||||
|
data["used"] = used;
|
||||||
|
data["size"] = size;
|
||||||
|
data["avail"] = avail;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/data/model/server_private_info.dart
Normal file
57
lib/data/model/server_private_info.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
|
||||||
|
///
|
||||||
|
class ServerPrivateInfo {
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"ip": "",
|
||||||
|
"port": 1,
|
||||||
|
"user": "",
|
||||||
|
"authorization": ""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
String? name;
|
||||||
|
String? ip;
|
||||||
|
int? port;
|
||||||
|
String? user;
|
||||||
|
Object? authorization;
|
||||||
|
|
||||||
|
ServerPrivateInfo({
|
||||||
|
this.name,
|
||||||
|
this.ip,
|
||||||
|
this.port,
|
||||||
|
this.user,
|
||||||
|
this.authorization,
|
||||||
|
});
|
||||||
|
ServerPrivateInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
name = json["name"]?.toString();
|
||||||
|
ip = json["ip"]?.toString();
|
||||||
|
port = json["port"]?.toInt();
|
||||||
|
user = json["user"]?.toString();
|
||||||
|
authorization = json["authorization"];
|
||||||
|
}
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data["name"] = name;
|
||||||
|
data["ip"] = ip;
|
||||||
|
data["port"] = port;
|
||||||
|
data["user"] = user;
|
||||||
|
data["authorization"] = authorization;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ServerPrivateInfo>? getServerInfoList(dynamic data) {
|
||||||
|
List<ServerPrivateInfo> ss = [];
|
||||||
|
if (data is String) {
|
||||||
|
data = json.decode(data);
|
||||||
|
}
|
||||||
|
for (var t in data) {
|
||||||
|
ss.add(ServerPrivateInfo.fromJson(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
90
lib/data/model/server_status.dart
Normal file
90
lib/data/model/server_status.dart
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:toolbox/data/model/disk_info.dart';
|
||||||
|
import 'package:toolbox/data/model/tcp_status.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
|
||||||
|
///
|
||||||
|
|
||||||
|
class ServerStatus {
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"cpuPercent": 0,
|
||||||
|
"memList": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"sysVer": "",
|
||||||
|
"uptime": "",
|
||||||
|
"disk": [
|
||||||
|
{
|
||||||
|
"mountPath": "",
|
||||||
|
"mountLocation": "",
|
||||||
|
"usedPercent": 0,
|
||||||
|
"used": "",
|
||||||
|
"size": "",
|
||||||
|
"avail": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
double? cpuPercent;
|
||||||
|
List<int?>? memList;
|
||||||
|
String? sysVer;
|
||||||
|
String? uptime;
|
||||||
|
List<DiskInfo?>? disk;
|
||||||
|
TcpStatus? tcp;
|
||||||
|
|
||||||
|
ServerStatus(
|
||||||
|
{this.cpuPercent,
|
||||||
|
this.memList,
|
||||||
|
this.sysVer,
|
||||||
|
this.uptime,
|
||||||
|
this.disk,
|
||||||
|
this.tcp});
|
||||||
|
ServerStatus.fromJson(Map<String, dynamic> json) {
|
||||||
|
cpuPercent = double.parse(json["cpuPercent"]);
|
||||||
|
if (json["memList"] != null) {
|
||||||
|
final v = json["memList"];
|
||||||
|
final arr0 = <int>[];
|
||||||
|
v.forEach((v) {
|
||||||
|
arr0.add(v.toInt());
|
||||||
|
});
|
||||||
|
memList = arr0;
|
||||||
|
}
|
||||||
|
sysVer = json["sysVer"]?.toString();
|
||||||
|
uptime = json["uptime"]?.toString();
|
||||||
|
if (json["disk"] != null) {
|
||||||
|
final v = json["disk"];
|
||||||
|
final arr0 = <DiskInfo>[];
|
||||||
|
v.forEach((v) {
|
||||||
|
arr0.add(DiskInfo.fromJson(v));
|
||||||
|
});
|
||||||
|
disk = arr0;
|
||||||
|
}
|
||||||
|
tcp = TcpStatus.fromJson(json['tcp']);
|
||||||
|
}
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data["cpuPercent"] = cpuPercent;
|
||||||
|
if (memList != null) {
|
||||||
|
final v = memList;
|
||||||
|
final arr0 = [];
|
||||||
|
for (var v in v!) {
|
||||||
|
arr0.add(v);
|
||||||
|
}
|
||||||
|
data["memList"] = arr0;
|
||||||
|
}
|
||||||
|
data["sysVer"] = sysVer;
|
||||||
|
data["uptime"] = uptime;
|
||||||
|
if (disk != null) {
|
||||||
|
final v = disk;
|
||||||
|
final arr0 = [];
|
||||||
|
for (var v in v!) {
|
||||||
|
arr0.add(v!.toJson());
|
||||||
|
}
|
||||||
|
data["disk"] = arr0;
|
||||||
|
}
|
||||||
|
data['tcp'] = tcp;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/data/model/tcp_status.dart
Normal file
39
lib/data/model/tcp_status.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
///
|
||||||
|
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
|
||||||
|
///
|
||||||
|
class TcpStatus {
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"maxConn": 0,
|
||||||
|
"active": 1,
|
||||||
|
"passive": 2,
|
||||||
|
"fail": 3
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
int? maxConn;
|
||||||
|
int? active;
|
||||||
|
int? passive;
|
||||||
|
int? fail;
|
||||||
|
|
||||||
|
TcpStatus({
|
||||||
|
this.maxConn,
|
||||||
|
this.active,
|
||||||
|
this.passive,
|
||||||
|
this.fail,
|
||||||
|
});
|
||||||
|
TcpStatus.fromJson(Map<String, dynamic> json) {
|
||||||
|
maxConn = json["maxConn"]?.toInt();
|
||||||
|
active = json["active"]?.toInt();
|
||||||
|
passive = json["passive"]?.toInt();
|
||||||
|
fail = json["fail"]?.toInt();
|
||||||
|
}
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = Map<String, dynamic>();
|
||||||
|
data["maxConn"] = maxConn;
|
||||||
|
data["active"] = active;
|
||||||
|
data["passive"] = passive;
|
||||||
|
data["fail"] = fail;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/data/provider/server.dart
Normal file
29
lib/data/provider/server.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:toolbox/core/provider_base.dart';
|
||||||
|
import 'package:toolbox/data/model/server_private_info.dart';
|
||||||
|
import 'package:toolbox/data/store/server.dart';
|
||||||
|
import 'package:toolbox/locator.dart';
|
||||||
|
|
||||||
|
class ServerProvider extends BusyProvider {
|
||||||
|
late List<ServerPrivateInfo> _servers;
|
||||||
|
|
||||||
|
List<ServerPrivateInfo> get servers => _servers;
|
||||||
|
|
||||||
|
Future<void> loadData() async {
|
||||||
|
setBusyState(true);
|
||||||
|
_servers = locator<ServerStore>().fetch();
|
||||||
|
setBusyState(false);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addServer(ServerPrivateInfo info) {
|
||||||
|
_servers.add(info);
|
||||||
|
locator<ServerStore>().put(info);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void delServer(ServerPrivateInfo info) {
|
||||||
|
_servers.remove(info);
|
||||||
|
locator<ServerStore>().delete(info);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,27 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:toolbox/core/persistant_store.dart';
|
||||||
|
import 'package:toolbox/data/model/server_private_info.dart';
|
||||||
|
|
||||||
|
class ServerStore extends PersistentStore {
|
||||||
|
void put(ServerPrivateInfo info) {
|
||||||
|
final ss = fetch();
|
||||||
|
if (!have(info)) ss.add(info);
|
||||||
|
box.put('servers', json.encode(ss));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ServerPrivateInfo> fetch() {
|
||||||
|
return getServerInfoList(
|
||||||
|
json.decode(box.get('servers', defaultValue: '[]')!))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete(ServerPrivateInfo s) {
|
||||||
|
final ss = fetch();
|
||||||
|
ss.removeWhere((e) => e.ip == s.ip && e.port == s.port && e.user == e.user);
|
||||||
|
box.put('servers', json.encode(ss));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool have(ServerPrivateInfo s) => fetch()
|
||||||
|
.where((e) => e.ip == s.ip && e.port == s.port && e.user == e.user)
|
||||||
|
.isNotEmpty;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:toolbox/data/provider/app.dart';
|
import 'package:toolbox/data/provider/app.dart';
|
||||||
import 'package:toolbox/data/provider/debug.dart';
|
import 'package:toolbox/data/provider/debug.dart';
|
||||||
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
import 'package:toolbox/data/service/app.dart';
|
import 'package:toolbox/data/service/app.dart';
|
||||||
|
import 'package:toolbox/data/store/server.dart';
|
||||||
import 'package:toolbox/data/store/setting.dart';
|
import 'package:toolbox/data/store/setting.dart';
|
||||||
|
|
||||||
GetIt locator = GetIt.instance;
|
GetIt locator = GetIt.instance;
|
||||||
@@ -13,12 +15,17 @@ void setupLocatorForServices() {
|
|||||||
void setupLocatorForProviders() {
|
void setupLocatorForProviders() {
|
||||||
locator.registerSingleton(AppProvider());
|
locator.registerSingleton(AppProvider());
|
||||||
locator.registerSingleton(DebugProvider());
|
locator.registerSingleton(DebugProvider());
|
||||||
|
locator.registerSingleton(ServerProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setupLocatorForStores() async {
|
Future<void> setupLocatorForStores() async {
|
||||||
final setting = SettingStore();
|
final setting = SettingStore();
|
||||||
await setting.init(boxName: 'setting');
|
await setting.init(boxName: 'setting');
|
||||||
locator.registerSingleton(setting);
|
locator.registerSingleton(setting);
|
||||||
|
|
||||||
|
final server = ServerStore();
|
||||||
|
await server.init(boxName: 'server');
|
||||||
|
locator.registerSingleton(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setupLocator() async {
|
Future<void> setupLocator() async {
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import 'package:toolbox/app.dart';
|
|||||||
import 'package:toolbox/core/analysis.dart';
|
import 'package:toolbox/core/analysis.dart';
|
||||||
import 'package:toolbox/data/provider/app.dart';
|
import 'package:toolbox/data/provider/app.dart';
|
||||||
import 'package:toolbox/data/provider/debug.dart';
|
import 'package:toolbox/data/provider/debug.dart';
|
||||||
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
import 'package:toolbox/locator.dart';
|
import 'package:toolbox/locator.dart';
|
||||||
|
|
||||||
Future<void> initApp() async {
|
Future<void> initApp() async {
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
await setupLocator();
|
await setupLocator();
|
||||||
|
locator<ServerProvider>().loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
void runInZone(dynamic Function() body) {
|
void runInZone(dynamic Function() body) {
|
||||||
@@ -51,6 +53,7 @@ Future<void> main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
|
ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
|
||||||
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
|
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
|
||||||
|
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
|
||||||
],
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ class DebugPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DebugPageState extends State<DebugPage> {
|
class _DebugPageState extends State<DebugPage> {
|
||||||
DebugProvider get debug => Provider.of<DebugProvider>(context);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -33,11 +31,12 @@ class _DebugPageState extends State<DebugPage> {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Consumer<DebugProvider>(builder: (_, debug, __) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: debug.widgets,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: debug.widgets);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import 'package:after_layout/after_layout.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:toolbox/core/route.dart';
|
import 'package:toolbox/core/route.dart';
|
||||||
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
import 'package:toolbox/data/res/build_data.dart';
|
import 'package:toolbox/data/res/build_data.dart';
|
||||||
|
import 'package:toolbox/locator.dart';
|
||||||
import 'package:toolbox/view/page/convert.dart';
|
import 'package:toolbox/view/page/convert.dart';
|
||||||
import 'package:toolbox/view/page/debug.dart';
|
import 'package:toolbox/view/page/debug.dart';
|
||||||
import 'package:toolbox/view/page/server.dart';
|
import 'package:toolbox/view/page/server.dart';
|
||||||
@@ -14,8 +18,11 @@ class MyHomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage>
|
class _MyHomePageState extends State<MyHomePage>
|
||||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
with
|
||||||
final List<String> _tabs = ['服务器', '编/解码', '1', '2', '3'];
|
AutomaticKeepAliveClientMixin,
|
||||||
|
SingleTickerProviderStateMixin,
|
||||||
|
AfterLayoutMixin {
|
||||||
|
final List<String> _tabs = ['服务器', '编/解码', '1', '2'];
|
||||||
late final TabController _tabController;
|
late final TabController _tabController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -45,7 +52,6 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
ConvertPage(),
|
ConvertPage(),
|
||||||
ConvertPage(),
|
ConvertPage(),
|
||||||
ConvertPage(),
|
ConvertPage(),
|
||||||
ConvertPage()
|
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -93,4 +99,10 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> afterFirstLayout(BuildContext context) async {
|
||||||
|
await GetIt.I.allReady();
|
||||||
|
await locator<ServerProvider>().loadData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
import 'package:after_layout/after_layout.dart';
|
||||||
import 'package:charts_flutter/flutter.dart' as chart;
|
import 'package:charts_flutter/flutter.dart' as chart;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:ssh2/ssh2.dart';
|
import 'package:ssh2/ssh2.dart';
|
||||||
|
import 'package:toolbox/core/extension/stringx.dart';
|
||||||
import 'package:toolbox/core/utils.dart';
|
import 'package:toolbox/core/utils.dart';
|
||||||
|
import 'package:toolbox/data/model/disk_info.dart';
|
||||||
|
import 'package:toolbox/data/model/server_private_info.dart';
|
||||||
|
import 'package:toolbox/data/model/server_status.dart';
|
||||||
|
import 'package:toolbox/data/model/tcp_status.dart';
|
||||||
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
|
import 'package:toolbox/locator.dart';
|
||||||
import 'package:toolbox/view/widget/circle_pie.dart';
|
import 'package:toolbox/view/widget/circle_pie.dart';
|
||||||
|
|
||||||
class ServerPage extends StatefulWidget {
|
class ServerPage extends StatefulWidget {
|
||||||
@@ -14,13 +24,29 @@ class ServerPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServerPageState extends State<ServerPage>
|
class _ServerPageState extends State<ServerPage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||||
late MediaQueryData _media;
|
late MediaQueryData _media;
|
||||||
late ThemeData _theme;
|
late ThemeData _theme;
|
||||||
|
bool useKey = false;
|
||||||
|
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
final ipController = TextEditingController();
|
||||||
|
final portController = TextEditingController();
|
||||||
|
final usernameController = TextEditingController();
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
final keyController = TextEditingController();
|
||||||
|
final ipFocusNode = FocusNode();
|
||||||
|
final portFocusNode = FocusNode();
|
||||||
|
final usernameFocusNode = FocusNode();
|
||||||
|
final passwordFocusNode = FocusNode();
|
||||||
|
|
||||||
|
late ServerProvider serverProvider;
|
||||||
|
final cachedServerStatus = <ServerStatus?>[];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
serverProvider = locator<ServerProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -38,123 +64,359 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||||
child: AnimationLimiter(
|
child: AnimationLimiter(
|
||||||
child: Column(
|
child: Consumer<ServerProvider>(builder: (_, pro, __) {
|
||||||
children: AnimationConfiguration.toStaggeredList(
|
return Column(
|
||||||
duration: const Duration(milliseconds: 377),
|
children: AnimationConfiguration.toStaggeredList(
|
||||||
childAnimationBuilder: (widget) => SlideAnimation(
|
duration: const Duration(milliseconds: 377),
|
||||||
verticalOffset: 50.0,
|
childAnimationBuilder: (widget) => SlideAnimation(
|
||||||
child: FadeInAnimation(
|
verticalOffset: 50.0,
|
||||||
child: widget,
|
child: FadeInAnimation(
|
||||||
|
child: widget,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
children: [
|
||||||
children: [const SizedBox(height: 13), ..._buildServerCards()],
|
const SizedBox(height: 13),
|
||||||
))),
|
...pro.servers
|
||||||
|
.map((e) => _buildEachServerCard(e, pro.servers.indexOf(e)))
|
||||||
|
],
|
||||||
|
));
|
||||||
|
})),
|
||||||
),
|
),
|
||||||
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
|
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showSnackBar(context, const Text(''));
|
showRoundDialog(context, '新建服务器连接', _buildTextInputField(context), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('关闭')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final authorization = keyController.text.isEmpty
|
||||||
|
? passwordController.text
|
||||||
|
: {
|
||||||
|
"privateKey": keyController.text,
|
||||||
|
"passphrase": passwordController.text
|
||||||
|
};
|
||||||
|
serverProvider.addServer(ServerPrivateInfo(
|
||||||
|
name: nameController.text,
|
||||||
|
ip: ipController.text,
|
||||||
|
port: int.parse(portController.text),
|
||||||
|
user: usernameController.text,
|
||||||
|
authorization: authorization));
|
||||||
|
nameController.clear();
|
||||||
|
ipController.clear();
|
||||||
|
portController.clear();
|
||||||
|
usernameController.clear();
|
||||||
|
passwordController.clear();
|
||||||
|
keyController.clear();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('连接'))
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
tooltip: 'add a server',
|
tooltip: 'add a server',
|
||||||
|
heroTag: 'server page fab',
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>>? _getData() async {
|
InputDecoration _buildDecoration(String label, {TextStyle? textStyle}) {
|
||||||
final client = SSHClient(
|
return InputDecoration(labelText: label, labelStyle: textStyle);
|
||||||
host: '',
|
}
|
||||||
port: 0,
|
|
||||||
username: '',
|
Widget _buildTextInputField(BuildContext ctx) {
|
||||||
passwordOrKey: '',
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: nameController,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: _buildDecoration('名称'),
|
||||||
|
onSubmitted: (_) =>
|
||||||
|
FocusScope.of(context).requestFocus(ipFocusNode),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: ipController,
|
||||||
|
focusNode: ipFocusNode,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: _buildDecoration('IP'),
|
||||||
|
onSubmitted: (_) =>
|
||||||
|
FocusScope.of(context).requestFocus(usernameFocusNode),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: portController,
|
||||||
|
focusNode: portFocusNode,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: _buildDecoration('Port'),
|
||||||
|
onSubmitted: (_) =>
|
||||||
|
FocusScope.of(context).requestFocus(usernameFocusNode),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: usernameController,
|
||||||
|
focusNode: usernameFocusNode,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: _buildDecoration('用户名'),
|
||||||
|
onSubmitted: (_) =>
|
||||||
|
FocusScope.of(context).requestFocus(passwordFocusNode),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: keyController,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: _buildDecoration('密钥(可选)'),
|
||||||
|
onSubmitted: (_) => {},
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: passwordController,
|
||||||
|
focusNode: passwordFocusNode,
|
||||||
|
obscureText: true,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: _buildDecoration('密码'),
|
||||||
|
onSubmitted: (_) => {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerStatus>? _getData(ServerPrivateInfo info) async {
|
||||||
|
final client = SSHClient(
|
||||||
|
host: info.ip!,
|
||||||
|
port: info.port!,
|
||||||
|
username: info.user!,
|
||||||
|
passwordOrKey: info.authorization,
|
||||||
|
);
|
||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
final cpu = await client.execute(
|
final cpu = await client.execute(
|
||||||
"top -bn1 | grep load | awk '{printf \"%.2f\", \$(NF-2)}'") ??
|
"top -bn1 | grep load | awk '{printf \"%.2f\", \$(NF-2)}'") ??
|
||||||
'failed';
|
'0';
|
||||||
final mem = await client
|
final mem = await client.execute('free -m') ?? '';
|
||||||
.execute("free -m | awk 'NR==2{printf \"%s/%sMB\", \$3,\$2}'") ??
|
final sysVer = await client.execute('cat /etc/issue.net') ?? 'Unkown';
|
||||||
'failed';
|
final upTime = await client.execute('uptime') ?? 'Failed';
|
||||||
return [cpu.trim(), mem.trim()];
|
final disk = await client.execute('df -h') ?? 'Failed';
|
||||||
|
final tcp = await client.execute('cat /proc/net/snmp') ?? 'Failed';
|
||||||
|
|
||||||
|
return ServerStatus(
|
||||||
|
cpuPercent: double.parse(cpu.trim()),
|
||||||
|
memList: _getMem(mem),
|
||||||
|
sysVer: sysVer.trim(),
|
||||||
|
disk: _getDisk(disk),
|
||||||
|
uptime: _getUpTime(upTime),
|
||||||
|
tcp: _getTcp(tcp));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEachServerCard() {
|
String _getUpTime(String raw) {
|
||||||
return FutureBuilder<List<String>>(
|
return raw.split('up ')[1].split(', ')[0];
|
||||||
future: _getData(),
|
}
|
||||||
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
TcpStatus _getTcp(String raw) {
|
||||||
if (snapshot.hasError) {
|
final lines = raw.split('\n');
|
||||||
return Text("Error: ${snapshot.error}");
|
int idx = 0;
|
||||||
} else {
|
for (var item in lines) {
|
||||||
return _buildEachCardContent(snapshot);
|
if (item.contains('Tcp:')) {
|
||||||
}
|
idx++;
|
||||||
} else {
|
}
|
||||||
return const CircularProgressIndicator();
|
if (idx == 2) {
|
||||||
}
|
final vals = item.split(RegExp(r'\s{1,}'));
|
||||||
|
return TcpStatus(
|
||||||
|
maxConn: vals[5].i,
|
||||||
|
active: vals[6].i,
|
||||||
|
passive: vals[7].i,
|
||||||
|
fail: vals[8].i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TcpStatus(maxConn: 0, active: 0, passive: 0, fail: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DiskInfo> _getDisk(String disk) {
|
||||||
|
final list = <DiskInfo>[];
|
||||||
|
final items = disk.split('\n');
|
||||||
|
for (var item in items) {
|
||||||
|
if (items.indexOf(item) == 0 || item.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final vals = item.split(RegExp(r'\s{1,}'));
|
||||||
|
list.add(DiskInfo(
|
||||||
|
mountPath: vals[1],
|
||||||
|
mountLocation: vals[5],
|
||||||
|
usedPercent: double.parse(vals[4].replaceFirst('%', '')),
|
||||||
|
used: vals[2],
|
||||||
|
size: vals[1],
|
||||||
|
avail: vals[3]));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _getMem(String mem) {
|
||||||
|
for (var item in mem.split('\n')) {
|
||||||
|
if (item.contains('Mem:')) {
|
||||||
|
return RegExp(r'[1-9][0-9]*')
|
||||||
|
.allMatches(item)
|
||||||
|
.map((e) => int.parse(item.substring(e.start, e.end)))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEachServerCard(ServerPrivateInfo e, int index) {
|
||||||
|
return FutureBuilder<ServerStatus>(
|
||||||
|
future: _getData(e),
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<ServerStatus> snapshot) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: _buildEachCardContent(snapshot, e.name ?? '', index),
|
||||||
|
onLongPress: () =>
|
||||||
|
showRoundDialog(context, '是否删除', const Text('删除后无法恢复'), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('否')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
serverProvider.delServer(e);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('是'))
|
||||||
|
]),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEachCardContent(AsyncSnapshot snapshot) {
|
Widget _buildEachCardContent(
|
||||||
final cpuPercent = double.parse(snapshot.data![0]) * 100;
|
AsyncSnapshot<ServerStatus> snapshot, String serverName, int index) {
|
||||||
final memSplit = snapshot.data![1].replaceFirst('MB', '').split('/');
|
Widget child;
|
||||||
final memPercent = int.parse(memSplit[0]) / int.parse(memSplit[1]) * 100;
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
final cpuData = [
|
if (cachedServerStatus.length > index && cachedServerStatus.elementAt(index) != null) {
|
||||||
IndexPercent(0, cpuPercent.toInt()),
|
child = _buildRealServerCard(cachedServerStatus.elementAt(index)!, serverName);
|
||||||
];
|
} else {
|
||||||
final memData = [
|
child = _buildRealServerCard(
|
||||||
IndexPercent(0, memPercent.toInt()),
|
ServerStatus(
|
||||||
];
|
cpuPercent: 0,
|
||||||
return Card(
|
memList: [100, 0],
|
||||||
child: Padding(padding:const EdgeInsets.all(13) , child: Column(
|
disk: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
DiskInfo(
|
||||||
|
mountLocation: '',
|
||||||
|
mountPath: '',
|
||||||
|
used: '',
|
||||||
|
size: '',
|
||||||
|
avail: '',
|
||||||
|
usedPercent: 0)
|
||||||
|
],
|
||||||
|
sysVer: '',
|
||||||
|
uptime: '',
|
||||||
|
tcp: TcpStatus(maxConn: 0, active: 0, passive: 0, fail: 0)),
|
||||||
|
serverName);
|
||||||
|
}
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
child = Column(
|
||||||
children: [
|
children: [
|
||||||
Text(' Jilin', style: TextStyle(fontWeight: FontWeight.bold),),
|
Text(
|
||||||
const SizedBox(height: 7,),
|
serverName,
|
||||||
Row(
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
),
|
||||||
children: [
|
Center(
|
||||||
_buildPercentCircle(cpuPercent, 'CPU', [
|
child: Text("Error: ${snapshot.error}"),
|
||||||
chart.Series<IndexPercent, int>(
|
|
||||||
id: 'CPU',
|
|
||||||
domainFn: (IndexPercent cpu, _) => cpu.id,
|
|
||||||
measureFn: (IndexPercent cpu, _) => cpu.percent,
|
|
||||||
data: cpuData,
|
|
||||||
)
|
)
|
||||||
]),
|
|
||||||
_buildPercentCircle(memPercent, 'MEM', [
|
|
||||||
chart.Series<IndexPercent, int>(
|
|
||||||
id: 'MEM',
|
|
||||||
domainFn: (IndexPercent sales, _) => sales.id,
|
|
||||||
measureFn: (IndexPercent sales, _) => sales.percent,
|
|
||||||
data: memData,
|
|
||||||
)
|
|
||||||
])
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),),
|
);
|
||||||
|
} else {
|
||||||
|
if (cachedServerStatus.length <= index) {
|
||||||
|
cachedServerStatus.add(snapshot.data!);
|
||||||
|
} else {
|
||||||
|
cachedServerStatus[index] = snapshot.data!;
|
||||||
|
}
|
||||||
|
child = _buildRealServerCard(snapshot.data!, serverName);
|
||||||
|
}
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(13),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPercentCircle(
|
Widget _buildRealServerCard(ServerStatus ss, String serverName) {
|
||||||
double percent, String title, List<chart.Series<IndexPercent, int>> series) {
|
final cpuData = [
|
||||||
|
IndexPercent(0, ss.cpuPercent!.toInt()),
|
||||||
|
IndexPercent(1, 100 - ss.cpuPercent!.toInt()),
|
||||||
|
];
|
||||||
|
final memData = <IndexPercent>[];
|
||||||
|
for (var e in ss.memList!) {
|
||||||
|
memData.add(IndexPercent(ss.memList!.indexOf(e), e!.toInt()));
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
serverName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(ss.uptime!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: _theme.textTheme.bodyText1!.color!.withAlpha(100)))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 13,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildPercentCircle(ss.cpuPercent!, 'CPU', [
|
||||||
|
chart.Series<IndexPercent, int>(
|
||||||
|
id: 'CPU',
|
||||||
|
domainFn: (IndexPercent cpu, _) => cpu.id,
|
||||||
|
measureFn: (IndexPercent cpu, _) => cpu.percent,
|
||||||
|
data: cpuData,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
_buildPercentCircle(
|
||||||
|
ss.memList![1]! / ss.memList![0]! * 100, 'Mem', [
|
||||||
|
chart.Series<IndexPercent, int>(
|
||||||
|
id: 'Mem',
|
||||||
|
domainFn: (IndexPercent sales, _) => sales.id,
|
||||||
|
measureFn: (IndexPercent sales, _) => sales.percent,
|
||||||
|
data: memData,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
_buildIOData('Net', ss.tcp!.maxConn!.toString(), '0kb/s'),
|
||||||
|
_buildIOData('Disk', '0kb/s', '0kb/s')
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIOData(String title, String up, String down) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _media.size.width * 0.2,
|
width: _media.size.width * 0.2,
|
||||||
height: _media.size.height * 0.1,
|
height: _media.size.height * 0.1,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
DonutPieChart.withRandomData(),
|
|
||||||
Positioned(
|
Positioned(
|
||||||
child: Text(
|
child: Column(
|
||||||
'${percent.toStringAsFixed(1)}%',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
|
Text(
|
||||||
|
'↓$up',
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'↑$down',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
top: _media.size.height * 0.012,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
|
||||||
bottom: 0
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
child: Text(title, textAlign: TextAlign.center),
|
child: Text(title, textAlign: TextAlign.center),
|
||||||
@@ -166,10 +428,39 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildServerCards() {
|
Widget _buildPercentCircle(double percent, String title,
|
||||||
return [_buildEachServerCard()];
|
List<chart.Series<IndexPercent, int>> series) {
|
||||||
|
return SizedBox(
|
||||||
|
width: _media.size.width * 0.2,
|
||||||
|
height: _media.size.height * 0.1,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
DonutPieChart(series),
|
||||||
|
Positioned(
|
||||||
|
child: Text(
|
||||||
|
'${percent.toStringAsFixed(1)}%',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: _media.size.height * 0.03,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
child: Text(title, textAlign: TextAlign.center),
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> afterFirstLayout(BuildContext context) async {
|
||||||
|
await GetIt.I.allReady();
|
||||||
|
await locator<ServerProvider>().loadData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
lib/view/widget/card_dialog.dart
Normal file
25
lib/view/widget/card_dialog.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CardDialog extends StatelessWidget {
|
||||||
|
const CardDialog(
|
||||||
|
{Key? key, this.title, this.content, this.actions, this.padding})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final Widget? content;
|
||||||
|
final List<Widget>? actions;
|
||||||
|
final Widget? title;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
contentPadding: padding ?? const EdgeInsets.fromLTRB(24, 17, 24, 7),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20.0)),
|
||||||
|
),
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
actions: actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
// EXCLUDE_FROM_GALLERY_DOCS_END
|
|
||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@@ -7,59 +5,28 @@ class DonutPieChart extends StatelessWidget {
|
|||||||
final List<charts.Series<dynamic, num>> seriesList;
|
final List<charts.Series<dynamic, num>> seriesList;
|
||||||
final bool animate;
|
final bool animate;
|
||||||
|
|
||||||
const DonutPieChart(this.seriesList, {Key? key, this.animate = true}) : super(key: key);
|
const DonutPieChart(this.seriesList, {Key? key, this.animate = false})
|
||||||
|
: super(key: key);
|
||||||
factory DonutPieChart.withRandomData() {
|
|
||||||
return DonutPieChart(_createRandomData());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create random data.
|
|
||||||
static List<charts.Series<IndexPercent, int>> _createRandomData() {
|
|
||||||
final random = Random();
|
|
||||||
|
|
||||||
final data = [
|
|
||||||
IndexPercent(0, random.nextInt(100)),
|
|
||||||
IndexPercent(1, random.nextInt(100)),
|
|
||||||
IndexPercent(2, random.nextInt(100)),
|
|
||||||
IndexPercent(3, random.nextInt(100)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
|
||||||
charts.Series<IndexPercent, int>(
|
|
||||||
id: 'Sales',
|
|
||||||
domainFn: (IndexPercent sales, _) => sales.id,
|
|
||||||
measureFn: (IndexPercent sales, _) => sales.percent,
|
|
||||||
data: data,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// EXCLUDE_FROM_GALLERY_DOCS_END
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return charts.PieChart(seriesList,
|
return charts.PieChart(seriesList,
|
||||||
animate: animate,
|
animate: animate,
|
||||||
layoutConfig: charts.LayoutConfig(
|
layoutConfig: charts.LayoutConfig(
|
||||||
leftMarginSpec: charts.MarginSpec.fixedPixel(1),
|
leftMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||||
topMarginSpec: charts.MarginSpec.fixedPixel(1),
|
topMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||||
rightMarginSpec: charts.MarginSpec.fixedPixel(1),
|
rightMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||||
bottomMarginSpec: charts.MarginSpec.fixedPixel(17)
|
bottomMarginSpec: charts.MarginSpec.fixedPixel(17)),
|
||||||
),
|
|
||||||
// Configure the width of the pie slices to 60px. The remaining space in
|
|
||||||
// the chart will be left as a hole in the center.
|
|
||||||
defaultRenderer: charts.ArcRendererConfig<num>(
|
defaultRenderer: charts.ArcRendererConfig<num>(
|
||||||
arcWidth: 6,
|
arcWidth: 6,
|
||||||
minHoleWidthForCenterContent: 60,
|
|
||||||
arcRatio: 0.2,
|
arcRatio: 0.2,
|
||||||
)
|
));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sample linear data type.
|
|
||||||
class IndexPercent {
|
class IndexPercent {
|
||||||
final int id;
|
final int id;
|
||||||
final int percent;
|
final int percent;
|
||||||
|
|
||||||
IndexPercent(this.id, this.percent);
|
IndexPercent(this.id, this.percent);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user