Compare commits

...

24 Commits
app ... v1.0.68

Author SHA1 Message Date
Junyuan Feng
7fb8c88ab8 detail page display memory exact value 2021-12-31 18:51:33 +08:00
Junyuan Feng
f480c49f1f scroll server connection failed info 2021-12-31 17:55:07 +08:00
Junyuan Feng
f7558d6beb can manually refresh when updateInterval==0 2021-12-31 17:44:17 +08:00
LollipopKit
de1e970108 Add r/w permission 2021-11-21 19:45:42 +08:00
LollipopKit
9ef59f4c12 Fix: app update issue on MIUI 2021-11-21 19:36:07 +08:00
LollipopKit
89ef2cb95c Will display the exception of Server Connection 2021-11-08 19:13:24 +08:00
LollipopKit
e0fb591dea Simply implement snippet running. 2021-11-06 14:05:03 +08:00
LollipopKit
7c34530821 Fix update things 2021-11-02 20:58:50 +08:00
LollipopKit
72c1901989 Add check update btn in setting 2021-11-02 20:31:44 +08:00
LollipopKit
ff76c6c539 Allow keep data on uninstalling 2021-11-02 19:49:29 +08:00
LollipopKit
d38bad7802 Update memList to Memory 2021-11-02 19:21:04 +08:00
LollipopKit
9e73dd07ca Optimized view logic 2021-11-02 15:48:24 +08:00
LollipopKit
3105552eae Init analysis only release mode 2021-11-02 15:48:04 +08:00
LollipopKit
ad05b296f6 Redesign net speed part 2021-11-02 15:29:17 +08:00
LollipopKit
f0a8941b59 Optimized net speed view 2021-11-01 22:19:32 +08:00
LollipopKit
fbc8f9598d Support to get net speed 2021-11-01 21:29:12 +08:00
LollipopKit
e7d87b40b8 Porgress use dynamic color 2021-11-01 15:26:29 +08:00
LollipopKit
1cd69c8f44 Fix: logical error when connecting to the server 2021-10-31 22:01:36 +08:00
LollipopKit
6e3fca32db Fix, Improve
- fix range exception when no data fetched from server
- display empty when no server stored
- server edit page auto select stored/used key item
2021-10-31 21:44:02 +08:00
LollipopKit
1943fde6eb remove useless function 2021-10-31 21:40:36 +08:00
LollipopKit
2eb6e19a86 Init snippet page and store 2021-10-31 15:22:05 +08:00
LollipopKit
9f3f07388e Enable analysis.Update dependencies.Update README 2021-10-31 15:21:23 +08:00
LollipopKit
702dd86a84 Add github action 2021-10-30 12:21:39 +08:00
LollipopKit
434ef77c03 Change Licence 2021-10-29 13:33:40 +08:00
42 changed files with 994 additions and 262 deletions

View File

@@ -12,7 +12,7 @@ A new Flutter project which provide a chart view to display server status data.
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3347.PNG"> <img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3347.PNG">
</td> </td>
<td> <td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3385.PNG"> <img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/detail.jpg">
</td> </td>
</tr> </tr>
</table> </table>
@@ -31,14 +31,28 @@ A new Flutter project which provide a chart view to display server status data.
</table> </table>
## Milestone ## Milestone
- [x] SSH Connect - [x] SSH connect
- [x] Server Info Store - [x] Server info store
- [x] Status Chart View - [x] Status chart view
- [x] Base64/Url En/Decode - [x] Base64/Url En/Decode
- [x] Private Key Store - [x] Private key store
- [x] Server Status Detail Page - [x] Server status detail page
- [x] Theme Switch - [x] Theme switch
- [ ] Execute Snippet - [x] Execute snippet
- [ ] Migrate from `ssh2` to `dartssh2`
- [ ] Desktop support
## Build
Please use `make.dart` to build.
```shell
# build android apk
./make.dart build android
# due to pub package 'ssh2' incompatibility
# can't build ios ipa through './make.dart build ios'
# more info: [https://github.com/jda258/flutter_ssh2/issues/8]
# please run below cmd to run on ios device
./make.dart run release
```
## License ## License
`Apache License. LollipopKit 2021` `LGPL License. LollipopKit 2021`

View File

@@ -1,8 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="tech.lolli.toolbox"> package="tech.lolli.toolbox">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application
android:label="ServerBox" android:label="ServerBox"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:hasFragileUserData="true"
android:restoreAnyVersion="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"

View File

@@ -5,6 +5,8 @@ PODS:
- GZ-NMSSH (4.1.5) - GZ-NMSSH (4.1.5)
- path_provider (0.0.1): - path_provider (0.0.1):
- Flutter - Flutter
- r_upgrade (0.0.1):
- Flutter
- ssh2 (2.2.3): - ssh2 (2.2.3):
- Flutter - Flutter
- GZ-NMSSH (~> 4.1.5) - GZ-NMSSH (~> 4.1.5)
@@ -15,6 +17,7 @@ DEPENDENCIES:
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`) - countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- path_provider (from `.symlinks/plugins/path_provider/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`)
- r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
- ssh2 (from `.symlinks/plugins/ssh2/ios`) - ssh2 (from `.symlinks/plugins/ssh2/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`)
@@ -29,6 +32,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
path_provider: path_provider:
:path: ".symlinks/plugins/path_provider/ios" :path: ".symlinks/plugins/path_provider/ios"
r_upgrade:
:path: ".symlinks/plugins/r_upgrade/ios"
ssh2: ssh2:
:path: ".symlinks/plugins/ssh2/ios" :path: ".symlinks/plugins/ssh2/ios"
url_launcher: url_launcher:
@@ -39,6 +44,7 @@ SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
GZ-NMSSH: d749f8ae2fd0094b953cd1d5abd8e0cab3c93f8d GZ-NMSSH: d749f8ae2fd0094b953cd1d5abd8e0cab3c93f8d
path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
ssh2: 74165efc99417a075ecafd52caf93edadfb5eb60 ssh2: 74165efc99417a075ecafd52caf93edadfb5eb60
url_launcher: b6e016d912f04be9f5bf6e8e82dc599b7ba59649 url_launcher: b6e016d912f04be9f5bf6e8e82dc599b7ba59649

View File

@@ -9,10 +9,6 @@ class Analysis {
static bool _enabled = false; static bool _enabled = false;
static Future<void> init(bool debug) async { static Future<void> init(bool debug) async {
if (_url.isEmpty || _key.isEmpty) {
return;
}
_enabled = true; _enabled = true;
await Countly.setLoggingEnabled(debug); await Countly.setLoggingEnabled(debug);
await Countly.init(_url, _key); await Countly.init(_url, _key);
@@ -22,12 +18,14 @@ class Analysis {
} }
static void recordView(String view) { static void recordView(String view) {
if (!_enabled) return; if (_enabled) {
Countly.recordView(view); Countly.recordView(view);
} }
}
static void recordException(Object exception, [bool fatal = false]) { static void recordException(Object exception, [bool fatal = false]) {
if (!_enabled) return; if (_enabled) {
Countly.logException(exception.toString(), !fatal, null); Countly.logException(exception.toString(), !fatal, null);
} }
} }
}

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:r_upgrade/r_upgrade.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart'; import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/build_data.dart';
@@ -39,8 +40,14 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
showSnackBarWithAction( showSnackBarWithAction(
context, context,
update.min > BuildData.build update.min > BuildData.build
? '您的版本过旧,请及时更新' ? 'Your version is too old. \nPlease update to v1.0.${update.newest}.'
: '${BuildData.name}有更新啦Ver${update.newest}\n${update.changelog}', : 'Update: v1.0.${update.newest} available. \n${update.changelog}',
'更新', 'Update', () async {
() => openUrl(Platform.isAndroid ? update.android : update.ios)); if (Platform.isAndroid) {
await RUpgrade.upgrade(update.android,
fileName: update.android.split('/').last, isAutoRequestInstall: true);
} else if (Platform.isIOS) {
showSnackBar(context, const Text('Not support iOS now.'));
}
});
} }

View File

@@ -15,13 +15,13 @@ bool isDarkMode(BuildContext context) =>
void showSnackBar(BuildContext context, Widget child) => void showSnackBar(BuildContext context, Widget child) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child)); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child));
void showSnackBarWithAction( void showSnackBarWithAction(BuildContext context, String content, String action,
BuildContext context, String content, String action, Function onTap) { GestureTapCallback onTap) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(content), content: Text(content),
action: SnackBarAction( action: SnackBarAction(
label: action, label: action,
onPressed: () => onTap, onPressed: onTap,
), ),
)); ));
} }

View File

@@ -33,28 +33,6 @@ class CpuStatus {
this.irq, this.irq,
this.softirq, this.softirq,
); );
CpuStatus.fromJson(Map<String, dynamic> json) {
id = json["id"];
user = json["user"]?.toInt();
sys = json["sys"]?.toInt();
nice = json["nice"]?.toInt();
idle = json["idle"]?.toInt();
iowait = json["iowait"]?.toInt();
irq = json["irq"]?.toInt();
softirq = json["softirq"]?.toInt();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["id"] = id;
data["user"] = user;
data["sys"] = sys;
data["nice"] = nice;
data["idle"] = idle;
data["iowait"] = iowait;
data["irq"] = irq;
data["softirq"] = softirq;
return data;
}
int get total => user + sys + nice + idle + iowait + irq + softirq; int get total => user + sys + nice + idle + iowait + irq + softirq;
} }

View File

@@ -25,23 +25,4 @@ class DiskInfo {
this.size, this.size,
this.avail, this.avail,
); );
DiskInfo.fromJson(Map<String, dynamic> json) {
mountPath = json["mountPath"].toString();
mountLocation = json["mountLocation"].toString();
usedPercent = int.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;
}
} }

View File

@@ -0,0 +1,15 @@
class Memory {
int total;
int used;
int free;
int shared;
int cache;
int avail;
Memory(
{required this.total,
required this.used,
required this.free,
required this.shared,
required this.cache,
required this.avail});
}

View File

@@ -0,0 +1,71 @@
import 'dart:math';
class NetSpeedPart {
String device;
int bytesIn;
int bytesOut;
int time;
NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);
}
class NetSpeed {
List<NetSpeedPart> old;
List<NetSpeedPart> now;
NetSpeed(this.old, this.now);
List<String> get devices {
final devices = <String>[];
for (var item in now) {
devices.add(item.device);
}
return devices;
}
NetSpeed update(List<NetSpeedPart> newOne) => NetSpeed(now, newOne);
int get timeDiff => now[0].time - old[0].time;
String speedIn({String? device}) {
if (old[0].device == '' || now[0].device == '') return '0kb/s';
int idx = 0;
if (device != null) {
for (var item in now) {
if (item.device == device) {
idx = now.indexOf(item);
break;
}
}
}
final speedInBytesPerSecond =
(now[idx].bytesIn - old[idx].bytesIn) / timeDiff;
int squareTimes = 0;
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
if (squareTimes >= suffixs.length - 1) break;
squareTimes++;
}
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
}
String speedOut({String? device}) {
if (old[0].device == '' || now[0].device == '') return '0kb/s';
int idx = 0;
if (device != null) {
for (var item in now) {
if (item.device == device) {
idx = now.indexOf(item);
break;
}
}
}
final speedInBytesPerSecond =
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
int squareTimes = 0;
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
if (squareTimes >= suffixs.length - 1) break;
squareTimes++;
}
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
}
}
const suffixs = ['b/s', 'kb/s', 'mb/s', 'gb/s'];

View File

@@ -35,7 +35,7 @@ class PrivateKeyInfo {
} }
} }
List<PrivateKeyInfo>? getPrivateKeyInfoList(dynamic data) { List<PrivateKeyInfo> getPrivateKeyInfoList(dynamic data) {
List<PrivateKeyInfo> ss = []; List<PrivateKeyInfo> ss = [];
if (data is String) { if (data is String) {
data = json.decode(data); data = json.decode(data);

View File

@@ -13,25 +13,27 @@ class ServerPrivateInfo {
} }
*/ */
String? name; late String name;
String? ip; late String ip;
int? port; late int port;
String? user; late String user;
Object? authorization; late Object authorization;
String? pubKeyId;
ServerPrivateInfo({ ServerPrivateInfo(
this.name, {required this.name,
this.ip, required this.ip,
this.port, required this.port,
this.user, required this.user,
this.authorization, required this.authorization,
}); this.pubKeyId});
ServerPrivateInfo.fromJson(Map<String, dynamic> json) { ServerPrivateInfo.fromJson(Map<String, dynamic> json) {
name = json["name"]?.toString(); name = json["name"].toString();
ip = json["ip"]?.toString(); ip = json["ip"].toString();
port = json["port"]?.toInt(); port = json["port"].toInt();
user = json["user"]?.toString(); user = json["user"].toString();
authorization = json["authorization"]; authorization = json["authorization"];
pubKeyId = json["pubKeyId"]?.toString();
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
@@ -40,11 +42,12 @@ class ServerPrivateInfo {
data["port"] = port; data["port"] = port;
data["user"] = user; data["user"] = user;
data["authorization"] = authorization; data["authorization"] = authorization;
data["pubKeyId"] = pubKeyId;
return data; return data;
} }
} }
List<ServerPrivateInfo>? getServerInfoList(dynamic data) { List<ServerPrivateInfo> getServerInfoList(dynamic data) {
List<ServerPrivateInfo> ss = []; List<ServerPrivateInfo> ss = [];
if (data is String) { if (data is String) {
data = json.decode(data); data = json.decode(data);

View File

@@ -1,5 +1,7 @@
import 'package:toolbox/data/model/server/cpu_2_status.dart'; import 'package:toolbox/data/model/server/cpu_2_status.dart';
import 'package:toolbox/data/model/server/disk_info.dart'; import 'package:toolbox/data/model/server/disk_info.dart';
import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
import 'package:toolbox/data/model/server/tcp_status.dart'; import 'package:toolbox/data/model/server/tcp_status.dart';
/// ///
@@ -28,13 +30,16 @@ class ServerStatus {
} }
*/ */
late Cpu2Status cpu2Status; Cpu2Status cpu2Status;
late List<int> memList; Memory memory;
late String sysVer; String sysVer;
late String uptime; String uptime;
late List<DiskInfo> disk; List<DiskInfo> disk;
late TcpStatus tcp; TcpStatus tcp;
NetSpeed netSpeed;
String? failedInfo;
ServerStatus(this.cpu2Status, this.memList, this.sysVer, this.uptime, ServerStatus(this.cpu2Status, this.memory, this.sysVer, this.uptime,
this.disk, this.tcp); this.disk, this.tcp, this.netSpeed,
{this.failedInfo});
} }

View File

@@ -0,0 +1,30 @@
import 'dart:convert';
class Snippet {
late String name;
late String script;
Snippet(this.name, this.script);
Snippet.fromJson(Map<String, dynamic> json) {
name = json['name'].toString();
script = json['script'].toString();
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['name'] = name;
data['script'] = script;
return data;
}
}
List<Snippet> getSnippetList(dynamic data) {
List<Snippet> ss = [];
if (data is String) {
data = json.decode(data);
}
for (var t in data) {
ss.add(Snippet.fromJson(t));
}
return ss;
}

View File

@@ -6,11 +6,14 @@ import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/provider_base.dart'; import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/server/cpu_2_status.dart'; import 'package:toolbox/data/model/server/cpu_2_status.dart';
import 'package:toolbox/data/model/server/cpu_status.dart'; import 'package:toolbox/data/model/server/cpu_status.dart';
import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/disk_info.dart'; import 'package:toolbox/data/model/server/disk_info.dart';
import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/model/server/tcp_status.dart'; import 'package:toolbox/data/model/server/tcp_status.dart';
import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
@@ -22,6 +25,14 @@ class ServerProvider extends BusyProvider {
final logger = Logger('ServerProvider'); final logger = Logger('ServerProvider');
Memory get emptyMemory =>
Memory(total: 1, used: 0, free: 1, shared: 0, cache: 0, avail: 1);
NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0);
NetSpeed get emptyNetSpeed =>
NetSpeed([emptyNetSpeedPart], [emptyNetSpeedPart]);
CpuStatus get emptyCpuStatus => CpuStatus('cpu', 0, 0, 0, 0, 0, 0, 0); CpuStatus get emptyCpuStatus => CpuStatus('cpu', 0, 0, 0, 0, 0, 0, 0);
Cpu2Status get emptyCpu2Status => Cpu2Status get emptyCpu2Status =>
@@ -29,11 +40,12 @@ class ServerProvider extends BusyProvider {
ServerStatus get emptyStatus => ServerStatus( ServerStatus get emptyStatus => ServerStatus(
emptyCpu2Status, emptyCpu2Status,
[100, 0], emptyMemory,
'', 'Loading...',
'', '',
[DiskInfo('/', '/', 0, '0', '0', '0')], [DiskInfo('/', '/', 0, '0', '0', '0')],
TcpStatus(0, 0, 0, 0)); TcpStatus(0, 0, 0, 0),
emptyNetSpeed);
Future<void> loadLocalData() async { Future<void> loadLocalData() async {
setBusyState(true); setBusyState(true);
@@ -50,9 +62,9 @@ class ServerProvider extends BusyProvider {
SSHClient genClient(ServerPrivateInfo spi) { SSHClient genClient(ServerPrivateInfo spi) {
return SSHClient( return SSHClient(
host: spi.ip!, host: spi.ip,
port: spi.port!, port: spi.port,
username: spi.user!, username: spi.user,
passwordOrKey: spi.authorization); passwordOrKey: spi.authorization);
} }
@@ -114,7 +126,9 @@ class ServerProvider extends BusyProvider {
final client = _servers[idx].client; final client = _servers[idx].client;
final connected = await client.isConnected(); final connected = await client.isConnected();
final state = _servers[idx].connectionState; final state = _servers[idx].connectionState;
if (!connected || state != ServerConnectionState.connected) { if (!connected ||
state == ServerConnectionState.failed ||
state == ServerConnectionState.disconnected) {
_servers[idx].connectionState = ServerConnectionState.connecting; _servers[idx].connectionState = ServerConnectionState.connecting;
notifyListeners(); notifyListeners();
final time1 = DateTime.now(); final time1 = DateTime.now();
@@ -127,6 +141,7 @@ class ServerProvider extends BusyProvider {
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed; _servers[idx].connectionState = ServerConnectionState.failed;
_servers[idx].status.failedInfo = e.toString().split(', ')[1];
notifyListeners(); notifyListeners();
logger.warning(e); logger.warning(e);
} }
@@ -142,6 +157,8 @@ class ServerProvider extends BusyProvider {
final upTime = await client.execute('uptime') ?? ''; final upTime = await client.execute('uptime') ?? '';
final disk = await client.execute('df -h') ?? ''; final disk = await client.execute('df -h') ?? '';
final tcp = await client.execute('cat /proc/net/snmp') ?? ''; final tcp = await client.execute('cat /proc/net/snmp') ?? '';
final netSpeed =
await client.execute('cat /proc/net/dev && date +%s') ?? '';
return ServerStatus( return ServerStatus(
_getCPU(cpu, _servers[idx].status.cpu2Status, cpuTemp), _getCPU(cpu, _servers[idx].status.cpu2Status, cpuTemp),
@@ -149,7 +166,8 @@ class ServerProvider extends BusyProvider {
_getSysVer(sysVer), _getSysVer(sysVer),
_getUpTime(upTime), _getUpTime(upTime),
_getDisk(disk), _getDisk(disk),
_getTcp(tcp)); _getTcp(tcp),
_getNetSpeed(netSpeed, _servers[idx].status.netSpeed));
} catch (e) { } catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed; _servers[idx].connectionState = ServerConnectionState.failed;
notifyListeners(); notifyListeners();
@@ -158,6 +176,30 @@ class ServerProvider extends BusyProvider {
} }
} }
/// [raw] example:
/// Inter-| Receive | Transmit
/// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901
NetSpeed _getNetSpeed(String raw, NetSpeed old) {
final split = raw.split('\n');
final deviceCount = split.length - 3;
if (deviceCount < 1) return emptyNetSpeed;
final time = int.parse(split[split.length - 2]);
final results = <NetSpeedPart>[];
for (int idx = 2; idx < deviceCount; idx++) {
final data = split[idx].trim().split(':');
final device = data.first;
final bytes = data.last.trim().split(' ');
bytes.removeWhere((element) => element == '');
final bytesIn = int.parse(bytes.first);
final bytesOut = int.parse(bytes[8]);
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
}
return old.update(results);
}
String _getSysVer(String raw) { String _getSysVer(String raw) {
final s = raw.split('='); final s = raw.split('=');
if (s.length == 2) { if (s.length == 2) {
@@ -233,15 +275,25 @@ class ServerProvider extends BusyProvider {
return list; return list;
} }
List<int> _getMem(String mem) { Memory _getMem(String mem) {
for (var item in mem.split('\n')) { for (var item in mem.split('\n')) {
if (item.contains('Mem:')) { if (item.contains('Mem:')) {
return RegExp(r'[1-9][0-9]*') final split = item.replaceFirst('Mem:', '').split(' ');
.allMatches(item) split.removeWhere((e) => e == '');
.map((e) => int.parse(item.substring(e.start, e.end))) final memList = split.map((e) => int.parse(e)).toList();
.toList(); return Memory(
total: memList[0],
used: memList[1],
free: memList[2],
shared: memList[3],
cache: memList[4],
avail: memList[5]);
} }
} }
return []; return emptyMemory;
}
Future<String?> runSnippet(int idx, Snippet snippet) {
return _servers[idx].client.execute(snippet.script);
} }
} }

View File

@@ -0,0 +1,32 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/store/snippet.dart';
import 'package:toolbox/locator.dart';
class SnippetProvider extends BusyProvider {
List<Snippet> get snippets => _snippets;
late List<Snippet> _snippets;
void loadData() {
_snippets = locator<SnippetStore>().fetch();
}
void addInfo(Snippet snippet) {
_snippets.add(snippet);
locator<SnippetStore>().put(snippet);
notifyListeners();
}
void delInfo(Snippet snippet) {
_snippets.removeWhere((e) => e.name == snippet.name);
locator<SnippetStore>().delete(snippet);
notifyListeners();
}
void updateInfo(Snippet old, Snippet newOne) {
final idx = _snippets.indexWhere((e) => e.name == old.name);
_snippets[idx] = newOne;
locator<SnippetStore>().update(old, newOne);
notifyListeners();
}
}

View File

@@ -2,8 +2,9 @@
class BuildData { class BuildData {
static const String name = "ToolBox"; static const String name = "ToolBox";
static const int build = 43; static const int build = 65;
static const String engine = "Flutter 2.5.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 18116933e7 (13 days ago) • 2021-10-15 10:46:35 -0700\nEngine • revision d3ea636dc5\nTools • Dart 2.14.4\n"; static const String engine =
static const String buildAt = "2021-10-28 21:14:38.326376"; "Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (2 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
static const int modifications = 2; static const String buildAt = "2021-12-31 15:55:47.350456";
static const int modifications = 7;
} }

View File

@@ -18,3 +18,4 @@ class DynamicColor {
} }
final mainColor = DynamicColor(Colors.black87, Colors.white70); final mainColor = DynamicColor(Colors.black87, Colors.white70);
final progressColor = DynamicColor(Colors.grey.shade100, Colors.grey);

View File

@@ -0,0 +1,3 @@
import 'package:flutter/widgets.dart';
final appIcon = Image.asset('assets/app_icon.png');

View File

@@ -0,0 +1,4 @@
import 'package:toolbox/data/model/server/linux_icon.dart';
final linuxIcons = LinuxIcons(
['ubuntu', 'arch', 'centos', 'debian', 'fedora', 'opensuse', 'kali']);

View File

@@ -1,4 +0,0 @@
import 'package:toolbox/data/model/server/linux_icon.dart';
final linuxIcons = LinuxIcons(['ubuntu', 'arch', 'centos', 'debian', 'fedora',
'opensuse', 'kali']);

View File

@@ -1,3 +1,5 @@
const backendUrl = 'https://v2.custed.lolli.tech'; const backendUrl = 'https://v2.custed.lolli.tech';
const baseUrl = backendUrl + '/res/toolbox'; const baseUrl = backendUrl + '/res/toolbox';
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq'; const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
const myGithub = 'https://github.com/LollipopKit';
const rainSunMeGithub = 'https://github.com/RainSunMe';

View File

@@ -12,7 +12,7 @@ class PrivateKeyStore extends PersistentStore {
List<PrivateKeyInfo> fetch() { List<PrivateKeyInfo> fetch() {
return getPrivateKeyInfoList( return getPrivateKeyInfoList(
json.decode(box.get('key', defaultValue: '[]')!))!; json.decode(box.get('key', defaultValue: '[]')!));
} }
void delete(PrivateKeyInfo s) { void delete(PrivateKeyInfo s) {

View File

@@ -12,7 +12,7 @@ class ServerStore extends PersistentStore {
List<ServerPrivateInfo> fetch() { List<ServerPrivateInfo> fetch() {
return getServerInfoList( return getServerInfoList(
json.decode(box.get('servers', defaultValue: '[]')!))!; json.decode(box.get('servers', defaultValue: '[]')!));
} }
void delete(ServerPrivateInfo s) { void delete(ServerPrivateInfo s) {

View File

@@ -0,0 +1,32 @@
import 'dart:convert';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/data/model/server/snippet.dart';
class SnippetStore extends PersistentStore {
void put(Snippet snippet) {
final ss = fetch();
if (!have(snippet)) ss.add(snippet);
box.put('snippet', json.encode(ss));
}
List<Snippet> fetch() {
return getSnippetList(json.decode(box.get('snippet', defaultValue: '[]')!));
}
void delete(Snippet s) {
final ss = fetch();
ss.removeAt(index(s));
box.put('snippet', json.encode(ss));
}
void update(Snippet old, Snippet newInfo) {
final ss = fetch();
ss[index(old)] = newInfo;
box.put('snippet', json.encode(ss));
}
int index(Snippet s) => fetch().indexWhere((e) => e.name == s.name);
bool have(Snippet s) => index(s) != -1;
}

View File

@@ -3,10 +3,12 @@ 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/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/service/app.dart'; import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/data/store/private_key.dart'; import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/data/store/snippet.dart';
GetIt locator = GetIt.instance; GetIt locator = GetIt.instance;
@@ -18,6 +20,7 @@ void setupLocatorForProviders() {
locator.registerSingleton(AppProvider()); locator.registerSingleton(AppProvider());
locator.registerSingleton(DebugProvider()); locator.registerSingleton(DebugProvider());
locator.registerSingleton(ServerProvider()); locator.registerSingleton(ServerProvider());
locator.registerSingleton(SnippetProvider());
locator.registerSingleton(PrivateKeyProvider()); locator.registerSingleton(PrivateKeyProvider());
} }
@@ -33,6 +36,10 @@ Future<void> setupLocatorForStores() async {
final key = PrivateKeyStore(); final key = PrivateKeyStore();
await key.init(boxName: 'key'); await key.init(boxName: 'key');
locator.registerSingleton(key); locator.registerSingleton(key);
final snippet = SnippetStore();
await snippet.init(boxName: 'snippet');
locator.registerSingleton(snippet);
} }
Future<void> setupLocator() async { Future<void> setupLocator() async {

View File

@@ -10,11 +10,13 @@ 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/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/snippet.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<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData(); locator<PrivateKeyProvider>().loadData();
///设置Logger ///设置Logger
@@ -62,6 +64,7 @@ Future<void> main() async {
ChangeNotifierProvider(create: (_) => locator<AppProvider>()), ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()), ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()), ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()), ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
], ],
child: const MyApp(), child: const MyApp(),

View File

@@ -1,17 +1,22 @@
import 'package:after_layout/after_layout.dart'; 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:get_it/get_it.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/build_mode.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/update.dart'; import 'package:toolbox/core/update.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/server.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/data/res/icon/common.dart';
import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/locator.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/private_key/stored.dart'; import 'package:toolbox/view/page/private_key/list.dart';
import 'package:toolbox/view/page/server/tab.dart'; import 'package:toolbox/view/page/server/tab.dart';
import 'package:toolbox/view/page/setting.dart'; import 'package:toolbox/view/page/setting.dart';
import 'package:toolbox/view/page/snippet/list.dart';
import 'package:toolbox/view/widget/url_text.dart'; import 'package:toolbox/view/widget/url_text.dart';
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
@@ -67,7 +72,7 @@ class _MyHomePageState extends State<MyHomePage>
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
children: [ children: [
UserAccountsDrawerHeader( UserAccountsDrawerHeader(
accountName: const Text('ToolBox'), accountName: const Text(BuildData.name),
accountEmail: Text(_buildVersionStr()), accountEmail: Text(_buildVersionStr()),
currentAccountPicture: _buildIcon(), currentAccountPicture: _buildIcon(),
), ),
@@ -80,7 +85,14 @@ class _MyHomePageState extends State<MyHomePage>
leading: const Icon(Icons.vpn_key), leading: const Icon(Icons.vpn_key),
title: const Text('Private Key'), title: const Text('Private Key'),
onTap: () => onTap: () =>
AppRoute(const StoredPrivateKeysPage(), 'Setting').go(context), AppRoute(const StoredPrivateKeysPage(), 'private key list')
.go(context),
),
ListTile(
leading: const Icon(Icons.snippet_folder),
title: const Text('Snippet'),
onTap: () =>
AppRoute(const SnippetListPage(), 'snippet list').go(context),
), ),
AboutListTile( AboutListTile(
icon: const Icon(Icons.text_snippet), icon: const Icon(Icons.text_snippet),
@@ -90,8 +102,12 @@ class _MyHomePageState extends State<MyHomePage>
applicationIcon: _buildIcon(), applicationIcon: _buildIcon(),
aboutBoxChildren: const [ aboutBoxChildren: const [
UrlText( UrlText(
text: '''\nMade with ❤️ by https://github.com/LollipopKit . text: '\nMade with ❤️ by $myGithub', replace: 'LollipopKit'),
\nAll rights reserved.''', replace: 'LollipopKit'), UrlText(
text:
'\nThanks $rainSunMeGithub for participating in the test.\n\nAll rights reserved.',
replace: 'RainSunMe',
),
], ],
), ),
], ],
@@ -102,7 +118,7 @@ class _MyHomePageState extends State<MyHomePage>
Widget _buildIcon() { Widget _buildIcon() {
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 60, maxWidth: 60), constraints: const BoxConstraints(maxHeight: 60, maxWidth: 60),
child: Image.asset('assets/app_icon.png'), child: appIcon,
); );
} }
@@ -118,5 +134,8 @@ class _MyHomePageState extends State<MyHomePage>
await GetIt.I.allReady(); await GetIt.I.allReady();
await locator<ServerProvider>().loadLocalData(); await locator<ServerProvider>().loadLocalData();
await doUpdate(context); await doUpdate(context);
if (BuildMode.isRelease) {
await Analysis.init(false);
}
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart'; import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
@@ -70,8 +71,15 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.send), child: const Icon(Icons.send),
onPressed: () { onPressed: () {
final info = PrivateKeyInfo( final name = nameController.text;
nameController.text, keyController.text, pwdController.text); final key = keyController.text;
final pwd = pwdController.text;
if (name.isEmpty || key.isEmpty || pwd.isEmpty) {
showSnackBar(
context, const Text('Three fields must not be empty.'));
return;
}
final info = PrivateKeyInfo(name, key, pwd);
if (widget.info != null) { if (widget.info != null) {
_provider.updateInfo(widget.info!, info); _provider.updateInfo(widget.info!, info);
} else { } else {

View File

@@ -1,12 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/linux_icons.dart'; import 'package:toolbox/data/res/icon/linux_icons.dart';
import 'package:toolbox/view/page/server/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/round_rect_card.dart';
const style11 = TextStyle(fontSize: 11);
const style13 = TextStyle(fontSize: 13);
class ServerDetailPage extends StatefulWidget { class ServerDetailPage extends StatefulWidget {
const ServerDetailPage(this.id, {Key? key}) : super(key: key); const ServerDetailPage(this.id, {Key? key}) : super(key: key);
@@ -37,7 +43,17 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildMainPage(ServerInfo si) { Widget _buildMainPage(ServerInfo si) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(si.info.name ?? 'Server Detail'), title: Text(si.info.name),
actions: [
IconButton(
onPressed: () => AppRoute(
ServerEditPage(
spi: si.info,
),
'Edit server info page')
.go(context),
icon: const Icon(Icons.edit))
],
), ),
body: ListView( body: ListView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
@@ -47,7 +63,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
_buildUpTimeAndSys(si.status), _buildUpTimeAndSys(si.status),
_buildCPUView(si.status), _buildCPUView(si.status),
_buildDiskView(si.status), _buildDiskView(si.status),
_buildMemView(si.status) _buildMemView(si.status),
_buildNetView(si.status.netSpeed)
], ],
), ),
); );
@@ -56,7 +73,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildLinuxIcon(String sysVer) { Widget _buildLinuxIcon(String sysVer) {
final iconPath = linuxIcons.search(sysVer); final iconPath = linuxIcons.search(sysVer);
if (iconPath == null) return const SizedBox(); if (iconPath == null) return const SizedBox();
return SizedBox(height: _media.size.height * 0.15, child: Image.asset(iconPath)); return SizedBox(
height: _media.size.height * 0.15, child: Image.asset(iconPath));
} }
Widget _buildCPUView(ServerStatus ss) { Widget _buildCPUView(ServerStatus ss) {
@@ -144,7 +162,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
return LinearProgressIndicator( return LinearProgressIndicator(
value: percentWithinOne, value: percentWithinOne,
minHeight: 7, minHeight: 7,
backgroundColor: Colors.grey[100], backgroundColor: progressColor.resolve(context),
color: pColor.withOpacity(0.5 + percentWithinOne / 2), color: pColor.withOpacity(0.5 + percentWithinOne / 2),
); );
} }
@@ -155,16 +173,30 @@ class _ServerDetailPageState extends State<ServerDetailPage>
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(ss.sysVer), Text(ss.sysVer, style: style11, textScaleFactor: 1.0),
Text(ss.uptime), Text(
ss.uptime,
style: style11,
textScaleFactor: 1.0,
),
], ],
), ),
)); ));
} }
String convertMB(int mb) {
const suffix = ['MB', 'GB', 'TB'];
double value = mb.toDouble();
int squareTimes = 0;
for (; value / 1024 > 1 && squareTimes < 3; squareTimes++) {
value /= 1024;
}
return '${value.toStringAsFixed(1)} ${suffix[squareTimes]}';
}
Widget _buildMemView(ServerStatus ss) { Widget _buildMemView(ServerStatus ss) {
final pColor = primaryColor; final pColor = primaryColor;
final used = ss.memList[1] / ss.memList[0]; final used = ss.memory.used / ss.memory.total;
final width = _media.size.width - 17 * 2 - 17 * 2; final width = _media.size.width - 17 * 2 - 17 * 2;
return RoundRectCard(SizedBox( return RoundRectCard(SizedBox(
height: 47, height: 47,
@@ -175,9 +207,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_buildMemExplain('Used', pColor), _buildMemExplain(convertMB(ss.memory.used), pColor),
_buildMemExplain('Cache', pColor.withAlpha(77)), _buildMemExplain(
_buildMemExplain('Avail', Colors.grey.shade100) convertMB(ss.memory.cache), pColor.withAlpha(77)),
_buildMemExplain(
convertMB(ss.memory.total - ss.memory.avail), progressColor.resolve(context))
], ],
), ),
const SizedBox( const SizedBox(
@@ -194,8 +228,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
SizedBox( SizedBox(
width: width * (1 - used), width: width * (1 - used),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: ss.memList[4] / ss.memList[0], // memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
backgroundColor: Colors.grey[100], value: ss.memory.total == 1
? 0
: ss.memory.cache / ss.memory.total,
backgroundColor: progressColor.resolve(context),
color: pColor.withAlpha(77), color: pColor.withAlpha(77),
), ),
) )
@@ -215,7 +252,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
width: 11, width: 11,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(type, style: const TextStyle(fontSize: 10), textScaleFactor: 1.0) Text(type, style: style11, textScaleFactor: 1.0)
], ],
); );
} }
@@ -244,12 +281,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
children: [ children: [
Text( Text(
'${disk.usedPercent}% of ${disk.size}', '${disk.usedPercent}% of ${disk.size}',
style: const TextStyle(fontSize: 11), style: style11,
textScaleFactor: 1.0, textScaleFactor: 1.0,
), ),
Text(disk.mountPath, Text(disk.mountPath, style: style11, textScaleFactor: 1.0)
style: const TextStyle(fontSize: 11),
textScaleFactor: 1.0)
], ],
), ),
_buildProgress(disk.usedPercent.toDouble()) _buildProgress(disk.usedPercent.toDouble())
@@ -260,5 +295,72 @@ class _ServerDetailPageState extends State<ServerDetailPage>
)); ));
} }
static const ignorePath = ['/run', '/sys', '/dev/shm', '/snap', '/var/lib/docker']; Widget _buildNetView(NetSpeed ns) {
final children = <Widget>[
_buildNetSpeedTop(),
const Divider(
height: 7,
)
];
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 7),
child: Column(
children: children,
),
));
}
Widget _buildNetSpeedTop() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.device_hub,
size: 17,
),
Icon(Icons.arrow_upward, size: 17),
Icon(Icons.arrow_downward, size: 17)
],
),
);
}
Widget _buildNetSpeedItem(NetSpeed ns, String device) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: _media.size.width / 4,
child: Text(device, style: style11, textScaleFactor: 1.0)),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedIn(device: device),
style: style11,
textAlign: TextAlign.center,
textScaleFactor: 1.0),
),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedOut(device: device),
style: style11,
textAlign: TextAlign.right,
textScaleFactor: 1.0))
],
),
);
}
static const ignorePath = [
'/run',
'/sys',
'/dev/shm',
'/snap',
'/var/lib/docker',
'/dev/tty'
];
} }

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
@@ -32,8 +33,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
bool usePublicKey = false; bool usePublicKey = false;
int _typeOptionIndex = -1; int _pubKeyIndex = -1;
final List<String> _keyInfo = ['', '']; late PrivateKeyInfo _keyInfo;
@override @override
void initState() { void initState() {
@@ -47,9 +48,27 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
appBar: AppBar(title: const Text('Edit'), actions: [ appBar: AppBar(title: const Text('Edit'), actions: [
widget.spi != null widget.spi != null
? IconButton( ? IconButton(
onPressed: () {
showRoundDialog(
context,
'Attention',
Text(
'Are you sure to delete server [${widget.spi!.name}]'),
[
TextButton(
onPressed: () { onPressed: () {
_serverProvider.delServer(widget.spi!); _serverProvider.delServer(widget.spi!);
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: const Text(
'Yes',
style: TextStyle(color: Colors.red),
)),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('No'))
]);
}, },
icon: const Icon(Icons.delete)) icon: const Icon(Icons.delete))
: const SizedBox() : const SizedBox()
@@ -109,13 +128,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
: const SizedBox(), : const SizedBox(),
usePublicKey usePublicKey
? Consumer<PrivateKeyProvider>(builder: (_, key, __) { ? Consumer<PrivateKeyProvider>(builder: (_, key, __) {
for (var item in key.infos) {
if (item.id == widget.spi?.pubKeyId) {
_pubKeyIndex = key.infos.indexOf(item);
}
}
final tiles = key.infos final tiles = key.infos
.map( .map(
(e) => ListTile( (e) => ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start), title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), trailing: _buildRadio(key.infos.indexOf(e), e)),
e.privateKey, e.password)),
) )
.toList(); .toList();
tiles.add(ListTile( tiles.add(ListTile(
@@ -155,7 +178,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
showSnackBar(context, const Text('Please enter password.')); showSnackBar(context, const Text('Please enter password.'));
return; return;
} }
if (usePublicKey && _typeOptionIndex == -1) { if (usePublicKey && _pubKeyIndex == -1) {
showSnackBar(context, const Text('Please select a private key.')); showSnackBar(context, const Text('Please select a private key.'));
return; return;
} }
@@ -166,14 +189,18 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
portController.text = '22'; portController.text = '22';
} }
final authorization = usePublicKey final authorization = usePublicKey
? {"privateKey": _keyInfo[0], "passphrase": _keyInfo[1]} ? {
"privateKey": _keyInfo.privateKey,
"passphrase": _keyInfo.password
}
: passwordController.text; : passwordController.text;
final spi = ServerPrivateInfo( final spi = ServerPrivateInfo(
name: nameController.text, name: nameController.text,
ip: ipController.text, ip: ipController.text,
port: int.parse(portController.text), port: int.parse(portController.text),
user: usernameController.text, user: usernameController.text,
authorization: authorization); authorization: authorization,
pubKeyId: usePublicKey ? _keyInfo.id : null);
if (widget.spi == null) { if (widget.spi == null) {
_serverProvider.addServer(spi); _serverProvider.addServer(spi);
@@ -187,15 +214,14 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
); );
} }
Radio _buildRadio(int index, String key, String pwd) { Radio _buildRadio(int index, PrivateKeyInfo pki) {
return Radio<int>( return Radio<int>(
value: index, value: index,
groupValue: _typeOptionIndex, groupValue: _pubKeyIndex,
onChanged: (int? value) { onChanged: (int? value) {
setState(() { setState(() {
_typeOptionIndex = value!; _pubKeyIndex = value!;
_keyInfo[0] = key; _keyInfo = pki;
_keyInfo[1] = pwd;
}); });
}, },
); );
@@ -214,10 +240,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final auth = widget.spi?.authorization as Map; final auth = widget.spi?.authorization as Map;
passwordController.text = auth['passphrase']; passwordController.text = auth['passphrase'];
keyController.text = auth['privateKey']; keyController.text = auth['privateKey'];
setState(() {
usePublicKey = true; usePublicKey = true;
}); }
} setState(() {});
} }
} }
} }

View File

@@ -1,15 +1,17 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:circle_chart/circle_chart.dart'; import 'package:circle_chart/circle_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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:get_it/get_it.dart';
import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/server/detail.dart'; import 'package:toolbox/view/page/server/detail.dart';
@@ -27,6 +29,7 @@ class _ServerPageState extends State<ServerPage>
late MediaQueryData _media; late MediaQueryData _media;
late ThemeData _theme; late ThemeData _theme;
late Color _primaryColor; late Color _primaryColor;
late RefreshController _refreshController;
late ServerProvider _serverProvider; late ServerProvider _serverProvider;
@@ -34,6 +37,7 @@ class _ServerPageState extends State<ServerPage>
void initState() { void initState() {
super.initState(); super.initState();
_serverProvider = locator<ServerProvider>(); _serverProvider = locator<ServerProvider>();
_refreshController = RefreshController();
} }
@override @override
@@ -41,22 +45,31 @@ class _ServerPageState extends State<ServerPage>
super.didChangeDependencies(); super.didChangeDependencies();
_media = MediaQuery.of(context); _media = MediaQuery.of(context);
_theme = Theme.of(context); _theme = Theme.of(context);
_primaryColor = Color(locator<SettingStore>().primaryColor.fetch()!); _primaryColor = primaryColor;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Scaffold( final autoUpdate =
body: SingleChildScrollView( locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return const Center(
child: Text(
'There is no server.\nClick the fab to add one.',
textAlign: TextAlign.center,
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7), padding: const EdgeInsets.symmetric(horizontal: 7),
child: AnimationLimiter( child: AnimationLimiter(
child: Consumer<ServerProvider>(builder: (_, pro, __) { child: Column(
return Column(
children: AnimationConfiguration.toStaggeredList( children: AnimationConfiguration.toStaggeredList(
duration: const Duration(milliseconds: 377), duration: const Duration(milliseconds: 777),
childAnimationBuilder: (widget) => SlideAnimation( childAnimationBuilder: (widget) => SlideAnimation(
verticalOffset: 50.0, verticalOffset: 77.0,
child: FadeInAnimation( child: FadeInAnimation(
child: widget, child: widget,
), ),
@@ -65,8 +78,19 @@ class _ServerPageState extends State<ServerPage>
const SizedBox(height: 13), const SizedBox(height: 13),
...pro.servers.map((e) => _buildEachServerCard(e)) ...pro.servers.map((e) => _buildEachServerCard(e))
], ],
)); ))),
})), );
});
return Scaffold(
body: autoUpdate
? child
: SmartRefresher(
controller: _refreshController,
child: child,
onRefresh: () async {
await _serverProvider.refreshData();
_refreshController.refreshCompleted();
},
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => onPressed: () =>
@@ -80,25 +104,18 @@ class _ServerPageState extends State<ServerPage>
} }
Widget _buildEachServerCard(ServerInfo si) { Widget _buildEachServerCard(ServerInfo si) {
return GestureDetector( return Card(
child: _buildEachCardContent(si), child: InkWell(
onLongPress: () { onLongPress: () => AppRoute(
AppRoute(
ServerEditPage( ServerEditPage(
spi: si.info, spi: si.info,
), ),
'Edit server info page') 'Edit server info page')
.go(context); .go(context),
});
}
Widget _buildEachCardContent(ServerInfo si) {
return Card(
child: InkWell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
child: _buildRealServerCard( child:
si.status, si.info.name ?? '', si.connectionState), _buildRealServerCard(si.status, si.info.name, si.connectionState),
), ),
onTap: () => onTap: () =>
AppRoute(ServerDetailPage(si.client.id!), 'server detail page') AppRoute(ServerDetailPage(si.client.id!), 'server detail page')
@@ -112,6 +129,12 @@ class _ServerPageState extends State<ServerPage>
final rootDisk = final rootDisk =
ss.disk.firstWhere((element) => element.mountLocation == '/'); ss.disk.firstWhere((element) => element.mountLocation == '/');
final topRightStr =
getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime, ss.failedInfo);
final hasError = cs == ServerConnectionState.failed;
final style = TextStyle(
color: _theme.textTheme.bodyText1!.color!.withAlpha(100), fontSize: 11);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -123,11 +146,18 @@ class _ServerPageState extends State<ServerPage>
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
textScaleFactor: 1.0, textScaleFactor: 1.0,
), ),
Text(getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime), hasError
textScaleFactor: 1.0, ? ConstrainedBox(
style: TextStyle( constraints: BoxConstraints(
color: _theme.textTheme.bodyText1!.color!.withAlpha(100), maxWidth: _media.size.width * 0.57, maxHeight: 17),
fontSize: 11)) child: Marquee(
accelerationDuration: const Duration(seconds: 3),
accelerationCurve: Curves.linear,
decelerationDuration: const Duration(seconds: 3),
decelerationCurve: Curves.linear,
text: topRightStr, textScaleFactor: 1.0, style: style),
)
: Text(topRightStr, style: style, textScaleFactor: 1.0),
], ],
), ),
const SizedBox( const SizedBox(
@@ -137,7 +167,7 @@ class _ServerPageState extends State<ServerPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_buildPercentCircle(ss.cpu2Status.usedPercent(), 'CPU'), _buildPercentCircle(ss.cpu2Status.usedPercent(), 'CPU'),
_buildPercentCircle(ss.memList[1] / ss.memList[0] * 100, 'Mem'), _buildPercentCircle(ss.memory.used / ss.memory.total * 100, 'Mem'),
_buildIOData('Net', 'Conn:\n' + ss.tcp.maxConn.toString(), _buildIOData('Net', 'Conn:\n' + ss.tcp.maxConn.toString(),
'Fail:\n' + ss.tcp.fail.toString()), 'Fail:\n' + ss.tcp.fail.toString()),
_buildIOData('Disk', 'Total:\n' + rootDisk.size, _buildIOData('Disk', 'Total:\n' + rootDisk.size,
@@ -148,7 +178,8 @@ class _ServerPageState extends State<ServerPage>
); );
} }
String getTopRightStr(ServerConnectionState cs, String temp, String upTime) { String getTopRightStr(ServerConnectionState cs, String temp, String upTime,
String? failedInfo) {
switch (cs) { switch (cs) {
case ServerConnectionState.disconnected: case ServerConnectionState.disconnected:
return 'Disconnected'; return 'Disconnected';
@@ -157,7 +188,7 @@ class _ServerPageState extends State<ServerPage>
case ServerConnectionState.connecting: case ServerConnectionState.connecting:
return 'Connecting...'; return 'Connecting...';
case ServerConnectionState.failed: case ServerConnectionState.failed:
return 'Failed'; return failedInfo ?? 'Failed';
default: default:
return 'Unknown State'; return 'Unknown State';
} }

View File

@@ -1,5 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/update.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
@@ -17,6 +21,7 @@ class _SettingPageState extends State<SettingPage> {
late int _selectedColorValue; late int _selectedColorValue;
double _intervalValue = 0; double _intervalValue = 0;
late Color priColor; late Color priColor;
static const textStyle = TextStyle(fontSize: 14);
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@@ -40,15 +45,46 @@ class _SettingPageState extends State<SettingPage> {
body: ListView( body: ListView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
children: [ children: [
RoundRectCard(_buildAppColorPreview()), _buildAppColorPreview(),
RoundRectCard( _buildUpdateInterval(),
ExpansionTile( _buildCheckUpdate()
].map((e) => RoundRectCard(e)).toList(),
),
);
}
Widget _buildCheckUpdate() {
return Consumer<AppProvider>(builder: (_, app, __) {
String display;
if (app.newestBuild != null) {
if (app.newestBuild! > BuildData.build) {
display = 'Found: v1.0.${app.newestBuild}, click to update';
} else {
display = 'Current: v1.0.${BuildData.build}is up to date';
}
} else {
display = 'Current: v1.0.${BuildData.build}';
}
return ListTile(
contentPadding: EdgeInsets.zero,
trailing: const Icon(Icons.keyboard_arrow_right),
title: Text(
display,
style: textStyle,
textAlign: TextAlign.start,
),
onTap: () => doUpdate(context, force: true));
});
}
Widget _buildUpdateInterval() {
return ExpansionTile(
tilePadding: EdgeInsets.zero, tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero, childrenPadding: EdgeInsets.zero,
textColor: priColor, textColor: priColor,
title: const Text( title: const Text(
'Server status update interval', 'Server status update interval',
style: TextStyle(fontSize: 14), style: textStyle,
textAlign: TextAlign.start, textAlign: TextAlign.start,
), ),
subtitle: const Text( subtitle: const Text(
@@ -77,16 +113,16 @@ class _SettingPageState extends State<SettingPage> {
height: 3, height: 3,
), ),
_intervalValue == 0.0 _intervalValue == 0.0
? const Text('You set to 0, will not update automatically.') ? const Text(
'You set to 0, will not update automatically.\nYou can pull to refresh manually.',
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
)
: const SizedBox(), : const SizedBox(),
const SizedBox( const SizedBox(
height: 13, height: 13,
) )
], ],
),
)
],
),
); );
} }
@@ -108,7 +144,7 @@ class _SettingPageState extends State<SettingPage> {
), ),
title: const Text( title: const Text(
'App primary color', 'App primary color',
style: TextStyle(fontSize: 14), style: textStyle,
)); ));
} }

View File

@@ -0,0 +1,91 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_decoration.dart';
class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
final Snippet? snippet;
@override
_SnippetEditPageState createState() => _SnippetEditPageState();
}
class _SnippetEditPageState extends State<SnippetEditPage>
with AfterLayoutMixin {
final nameController = TextEditingController();
final scriptController = TextEditingController();
late SnippetProvider _provider;
@override
void initState() {
super.initState();
_provider = locator<SnippetProvider>();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [
widget.snippet != null
? IconButton(
onPressed: () {
_provider.delInfo(widget.snippet!);
Navigator.of(context).pop();
},
icon: const Icon(Icons.delete))
: const SizedBox()
]),
body: ListView(
padding: const EdgeInsets.all(13),
children: [
TextField(
controller: nameController,
keyboardType: TextInputType.text,
decoration: buildDecoration('Name', icon: Icons.info),
),
TextField(
controller: scriptController,
autocorrect: false,
minLines: 3,
maxLines: 10,
keyboardType: TextInputType.text,
enableSuggestions: false,
decoration: buildDecoration('Snippet', icon: Icons.code),
),
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.send),
onPressed: () {
final name = nameController.text;
final script = scriptController.text;
if (name.isEmpty || script.isEmpty) {
showSnackBar(context, const Text('Two fields must not be empty.'));
return;
}
final snippet = Snippet(name, script);
if (widget.snippet != null) {
_provider.updateInfo(widget.snippet!, snippet);
} else {
_provider.addInfo(snippet);
}
Navigator.of(context).pop();
},
),
);
}
@override
void afterFirstLayout(BuildContext context) {
if (widget.snippet != null) {
nameController.text = widget.snippet!.name;
scriptController.text = widget.snippet!.script;
}
}
}

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/snippet/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
class SnippetListPage extends StatefulWidget {
const SnippetListPage({Key? key}) : super(key: key);
@override
_SnippetListPageState createState() => _SnippetListPageState();
}
class _SnippetListPageState extends State<SnippetListPage> {
int _selectedIndex = 0;
final _textStyle = TextStyle(color: primaryColor);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Snippet List'),
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () =>
AppRoute(const SnippetEditPage(), 'snippet edit page').go(context),
),
);
}
Widget _buildBody() {
return Consumer<SnippetProvider>(
builder: (_, key, __) {
return key.snippets.isNotEmpty
? ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.snippets.length,
itemExtent: 57,
itemBuilder: (context, idx) {
return RoundRectCard(Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
key.snippets[idx].name,
textAlign: TextAlign.center,
),
Row(children: [
TextButton(
onPressed: () => AppRoute(
SnippetEditPage(snippet: key.snippets[idx]),
'snippet edit page')
.go(context),
child: Text(
'Edit',
style: _textStyle,
)),
TextButton(
onPressed: () => _showRunDialog(key.snippets[idx]),
child: Text(
'Run',
style: _textStyle,
))
])
],
));
})
: const Center(child: Text('No saved snippets.'));
},
);
}
void _showRunDialog(Snippet snippet) {
showRoundDialog(context, 'Choose destination',
Consumer<ServerProvider>(builder: (_, provider, __) {
if (provider.servers.isEmpty) {
return const Text('No server available');
}
return SizedBox(
height: 111,
child: Stack(children: [
Positioned(
child: Container(
height: 37,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(7)),
color: Colors.black12,
),
),
top: 36,
bottom: 36,
left: 0,
right: 0,
),
ListWheelScrollView.useDelegate(
itemExtent: 37,
diameterRatio: 1.2,
controller: FixedExtentScrollController(initialItem: 0),
onSelectedItemChanged: (idx) => _selectedIndex = idx,
physics: const FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) => Center(
child: Text(
provider.servers[index].info.name,
textAlign: TextAlign.center,
),
),
childCount: provider.servers.length),
)
]));
}), [
TextButton(
onPressed: () async {
final result = await locator<ServerProvider>()
.runSnippet(_selectedIndex, snippet);
if (result != null) {
showRoundDialog(context, 'Result',
Text(result, style: const TextStyle(fontSize: 13)), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'))
]);
}
},
child: const Text('Run')),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
]);
}
}

View File

@@ -96,6 +96,7 @@ Future<void> flutterBuild(String source, String target, bool isAndroid) async {
} else { } else {
print(buildResult.stderr.toString()); print(buildResult.stderr.toString());
print('\nBuild failed with exit code $exitCode'); print('\nBuild failed with exit code $exitCode');
exit(exitCode);
} }
final endTime = DateTime.now(); final endTime = DateTime.now();
print('Spent time: ${endTime.difference(startTime).toString()}'); print('Spent time: ${endTime.difference(startTime).toString()}');

View File

@@ -28,7 +28,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.1" version: "2.8.2"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -42,7 +42,7 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@@ -55,7 +55,7 @@ packages:
description: description:
path: "." path: "."
ref: main ref: main
resolved-ref: f3e0088bb08c5d05473b1d568943f4f8732dd984 resolved-ref: "36a46aaa41690aac96fa808a6e75841464007a3b"
url: "https://github.com/LollipopKit/circle_chart" url: "https://github.com/LollipopKit/circle_chart"
source: git source: git
version: "0.0.3" version: "0.0.3"
@@ -109,7 +109,7 @@ packages:
name: extended_image name: extended_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.1" version: "5.1.3"
extended_image_library: extended_image_library:
dependency: transitive dependency: transitive
description: description:
@@ -117,6 +117,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
fading_edge_scrollview:
dependency: transitive
description:
name: fading_edge_scrollview
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -163,7 +170,7 @@ packages:
name: flutter_native_splash name: flutter_native_splash
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.4" version: "1.3.1"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@@ -258,13 +265,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
marquee:
dependency: "direct main"
description:
name: marquee
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.10" version: "0.12.11"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -292,7 +306,7 @@ packages:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -363,6 +377,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.1" version: "6.0.1"
pull_to_refresh:
dependency: "direct main"
description:
name: pull_to_refresh
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
r_upgrade:
dependency: "direct main"
description:
name: r_upgrade
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.6"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -416,7 +444,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.2" version: "0.4.3"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -486,7 +514,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@@ -37,7 +37,7 @@ dependencies:
dio: ^4.0.0 dio: ^4.0.0
after_layout: ^1.1.0 after_layout: ^1.1.0
flutter_staggered_animations: ^1.0.0 flutter_staggered_animations: ^1.0.0
extended_image: ^4.0.0 extended_image: ^5.1.3
url_launcher: ^6.0.9 url_launcher: ^6.0.9
countly_flutter: countly_flutter:
git: git:
@@ -51,6 +51,9 @@ dependencies:
url: https://github.com/LollipopKit/circle_chart url: https://github.com/LollipopKit/circle_chart
ref: main ref: main
clipboard: ^0.1.3 clipboard: ^0.1.3
r_upgrade: ^0.3.6
pull_to_refresh: ^2.0.0
marquee: ^2.2.0
dev_dependencies: dev_dependencies:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

BIN
screenshots/detail.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB