mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
new & opt
new: support set maxRetryCount of server reconnection opt: server detail UI opt: server provider opt: `ssh` page on Android
This commit is contained in:
@@ -356,7 +356,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 213;
|
CURRENT_PROJECT_VERSION = 214;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -364,7 +364,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.213;
|
MARKETING_VERSION = 1.0.214;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -486,7 +486,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 213;
|
CURRENT_PROJECT_VERSION = 214;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -494,7 +494,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.213;
|
MARKETING_VERSION = 1.0.214;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -510,7 +510,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 213;
|
CURRENT_PROJECT_VERSION = 214;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -518,7 +518,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.213;
|
MARKETING_VERSION = 1.0.214;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
|||||||
24
lib/app.dart
24
lib/app.dart
@@ -64,17 +64,19 @@ class MyApp extends StatelessWidget {
|
|||||||
radioTheme: radioTheme,
|
radioTheme: radioTheme,
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData.dark().copyWith(
|
darkTheme: ThemeData.dark().copyWith(
|
||||||
useMaterial3: false,
|
useMaterial3: false,
|
||||||
floatingActionButtonTheme: fabTheme,
|
floatingActionButtonTheme: fabTheme,
|
||||||
iconTheme: iconTheme,
|
iconTheme: iconTheme,
|
||||||
primaryIconTheme: iconTheme,
|
primaryIconTheme: iconTheme,
|
||||||
switchTheme: switchTheme,
|
switchTheme: switchTheme,
|
||||||
inputDecorationTheme: inputDecorationTheme,
|
inputDecorationTheme: inputDecorationTheme,
|
||||||
radioTheme: radioTheme,
|
radioTheme: radioTheme,
|
||||||
colorScheme: ColorScheme.fromSwatch(
|
colorScheme: ColorScheme.fromSwatch(
|
||||||
primarySwatch: primaryColor.materialColor,
|
primarySwatch: primaryColor.materialColor,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
accentColor: primaryColor)),
|
accentColor: primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
home: const MyHomePage(),
|
home: const MyHomePage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ class Server {
|
|||||||
ServerPrivateInfo spi;
|
ServerPrivateInfo spi;
|
||||||
ServerStatus status;
|
ServerStatus status;
|
||||||
SSHClient? client;
|
SSHClient? client;
|
||||||
ServerConnectionState cs;
|
ServerState cs;
|
||||||
|
|
||||||
Server(this.spi, this.status, this.client, this.cs);
|
Server(this.spi, this.status, this.client, this.cs);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ServerConnectionState { disconnected, connecting, connected, failed }
|
enum ServerState {
|
||||||
|
disconnected,
|
||||||
|
connecting,
|
||||||
|
connected,
|
||||||
|
failed;
|
||||||
|
|
||||||
|
bool get shouldConnect =>
|
||||||
|
this == ServerState.disconnected || this == ServerState.failed;
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,15 +31,17 @@ class ServerPrivateInfo {
|
|||||||
@HiveField(5)
|
@HiveField(5)
|
||||||
String? pubKeyId;
|
String? pubKeyId;
|
||||||
|
|
||||||
String get id => '$user@$ip:$port';
|
late String id;
|
||||||
|
|
||||||
|
ServerPrivateInfo({
|
||||||
|
required this.name,
|
||||||
|
required this.ip,
|
||||||
|
required this.port,
|
||||||
|
required this.user,
|
||||||
|
required this.pwd,
|
||||||
|
this.pubKeyId,
|
||||||
|
}) : id = '$user@$ip:$port';
|
||||||
|
|
||||||
ServerPrivateInfo(
|
|
||||||
{required this.name,
|
|
||||||
required this.ip,
|
|
||||||
required this.port,
|
|
||||||
required this.user,
|
|
||||||
required this.pwd,
|
|
||||||
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();
|
||||||
@@ -47,7 +49,9 @@ class ServerPrivateInfo {
|
|||||||
user = json["user"].toString();
|
user = json["user"].toString();
|
||||||
pwd = json["authorization"].toString();
|
pwd = json["authorization"].toString();
|
||||||
pubKeyId = json["pubKeyId"]?.toString();
|
pubKeyId = json["pubKeyId"]?.toString();
|
||||||
|
id = '$user@$ip:$port';
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
data["name"] = name;
|
data["name"] = name;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:toolbox/data/res/misc.dart';
|
|
||||||
|
|
||||||
import '../../core/extension/uint8list.dart';
|
import '../../core/extension/uint8list.dart';
|
||||||
import '../../core/provider_base.dart';
|
import '../../core/provider_base.dart';
|
||||||
@@ -28,7 +27,7 @@ class ServerProvider extends BusyProvider {
|
|||||||
|
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
final logger = Logger('SERVER');
|
final _logger = Logger('SERVER');
|
||||||
|
|
||||||
Future<void> loadLocalData() async {
|
Future<void> loadLocalData() async {
|
||||||
setBusyState(true);
|
setBusyState(true);
|
||||||
@@ -39,7 +38,7 @@ class ServerProvider extends BusyProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Server genServer(ServerPrivateInfo spi) {
|
Server genServer(ServerPrivateInfo spi) {
|
||||||
return Server(spi, initStatus, null, ServerConnectionState.disconnected);
|
return Server(spi, initStatus, null, ServerState.disconnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshData({ServerPrivateInfo? spi}) async {
|
Future<void> refreshData({ServerPrivateInfo? spi}) async {
|
||||||
@@ -71,8 +70,9 @@ class ServerProvider extends BusyProvider {
|
|||||||
|
|
||||||
void setDisconnected() {
|
void setDisconnected() {
|
||||||
for (var i = 0; i < _servers.length; i++) {
|
for (var i = 0; i < _servers.length; i++) {
|
||||||
_servers[i].cs = ServerConnectionState.disconnected;
|
_servers[i].cs = ServerState.disconnected;
|
||||||
}
|
}
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void closeServer({ServerPrivateInfo? spi}) {
|
void closeServer({ServerPrivateInfo? spi}) {
|
||||||
@@ -81,163 +81,171 @@ class ServerProvider extends BusyProvider {
|
|||||||
_servers[i].client?.close();
|
_servers[i].client?.close();
|
||||||
_servers[i].client = null;
|
_servers[i].client = null;
|
||||||
}
|
}
|
||||||
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final idx = _servers.indexWhere((e) => e.spi == spi);
|
final idx = getServerIdx(spi.id);
|
||||||
if (idx < 0) {
|
|
||||||
throw RangeError.index(idx, _servers);
|
|
||||||
}
|
|
||||||
_servers[idx].client?.close();
|
_servers[idx].client?.close();
|
||||||
|
_servers[idx].client = null;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addServer(ServerPrivateInfo spi) {
|
void addServer(ServerPrivateInfo spi) {
|
||||||
_servers.add(genServer(spi));
|
_servers.add(genServer(spi));
|
||||||
locator<ServerStore>().put(spi);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
locator<ServerStore>().put(spi);
|
||||||
refreshData(spi: spi);
|
refreshData(spi: spi);
|
||||||
}
|
}
|
||||||
|
|
||||||
void delServer(ServerPrivateInfo info) {
|
void delServer(String id) {
|
||||||
final idx = _servers.indexWhere((s) => s.spi == info);
|
final idx = getServerIdx(id);
|
||||||
if (idx == -1) return;
|
|
||||||
_servers[idx].client?.close();
|
_servers[idx].client?.close();
|
||||||
_servers.removeAt(idx);
|
_servers.removeAt(idx);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
locator<ServerStore>().delete(info);
|
locator<ServerStore>().delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateServer(
|
Future<void> updateServer(
|
||||||
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
|
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
|
||||||
final idx = _servers.indexWhere((e) => e.spi == old);
|
final idx = _servers.indexWhere((e) => e.spi.id == old.id);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
throw RangeError.index(idx, _servers);
|
throw RangeError.index(idx, _servers);
|
||||||
}
|
}
|
||||||
_servers[idx].spi = newSpi;
|
_servers[idx].spi = newSpi;
|
||||||
locator<ServerStore>().update(old, newSpi);
|
|
||||||
_servers[idx].client = await genClient(newSpi);
|
_servers[idx].client = await genClient(newSpi);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
locator<ServerStore>().update(old, newSpi);
|
||||||
refreshData(spi: newSpi);
|
refreshData(spi: newSpi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getServerIdx(String id) {
|
||||||
|
final idx = _servers.indexWhere((s) => s.spi.id == id);
|
||||||
|
if (idx < 0) {
|
||||||
|
throw Exception('Server not found: $id');
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Server getServer(String id) => _servers[getServerIdx(id)];
|
||||||
|
|
||||||
Future<void> _getData(ServerPrivateInfo spi) async {
|
Future<void> _getData(ServerPrivateInfo spi) async {
|
||||||
final s = _servers.firstWhere((element) => element.spi == spi);
|
final sid = spi.id;
|
||||||
|
final s = getServer(sid);
|
||||||
final state = s.cs;
|
final state = s.cs;
|
||||||
if (state == ServerConnectionState.failed ||
|
if (state.shouldConnect) {
|
||||||
state == ServerConnectionState.disconnected) {
|
if (!_limiter.shouldTry(sid)) {
|
||||||
if (!_limiter.shouldTry(spi.id)) {
|
s.cs = ServerState.failed;
|
||||||
s.cs = ServerConnectionState.failed;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.cs = ServerConnectionState.connecting;
|
s.cs = ServerState.connecting;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final time1 = DateTime.now();
|
|
||||||
try {
|
try {
|
||||||
|
// try to connect
|
||||||
|
final time1 = DateTime.now();
|
||||||
s.client = await genClient(spi);
|
s.client = await genClient(spi);
|
||||||
final time2 = DateTime.now();
|
final time2 = DateTime.now();
|
||||||
logger.info(
|
final spentTime = time2.difference(time1).inMilliseconds;
|
||||||
'Connected to [${spi.name}] in [${time2.difference(time1).toString()}].');
|
_logger.info('Connected to [$sid] in $spentTime ms.');
|
||||||
s.cs = ServerConnectionState.connected;
|
|
||||||
|
// after connected
|
||||||
|
s.cs = ServerState.connected;
|
||||||
final writeResult = await s.client!
|
final writeResult = await s.client!
|
||||||
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath")
|
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath")
|
||||||
.string;
|
.string;
|
||||||
|
|
||||||
|
// if write failed
|
||||||
if (writeResult.isNotEmpty) {
|
if (writeResult.isNotEmpty) {
|
||||||
throw Exception(writeResult);
|
throw Exception(writeResult);
|
||||||
}
|
}
|
||||||
_limiter.resetTryTimes(spi.id);
|
_limiter.resetTryTimes(sid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
s.cs = ServerConnectionState.failed;
|
s.cs = ServerState.failed;
|
||||||
s.status.failedInfo = '$e ## ';
|
s.status.failedInfo = '$e';
|
||||||
logger.warning(e);
|
_logger.warning(e);
|
||||||
} finally {
|
} finally {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if client is null, return
|
|
||||||
if (s.client == null) return;
|
if (s.client == null) return;
|
||||||
|
// run script to get server status
|
||||||
final raw = await s.client!.run("sh $shellPath").string;
|
final raw = await s.client!.run("sh $shellPath").string;
|
||||||
final segments = raw.split(seperator).map((e) => e.trim()).toList();
|
final segments = raw.split(seperator).map((e) => e.trim()).toList();
|
||||||
if (raw.isEmpty || segments.length == 1) {
|
if (raw.isEmpty || segments.length == 1) {
|
||||||
s.cs = ServerConnectionState.failed;
|
s.cs = ServerState.failed;
|
||||||
if (s.status.failedInfo == null || s.status.failedInfo!.isEmpty) {
|
if (s.status.failedInfo == null || s.status.failedInfo!.isEmpty) {
|
||||||
s.status.failedInfo = 'No data received';
|
s.status.failedInfo = 'No data received';
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// remove first empty segment
|
||||||
segments.removeAt(0);
|
segments.removeAt(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_getCPU(spi, segments[2], segments[7], segments[8]);
|
_getCPU(sid, segments[2], segments[7], segments[8]);
|
||||||
_getMem(spi, segments[6]);
|
_getMem(sid, segments[6]);
|
||||||
_getSysVer(spi, segments[1]);
|
_getSysVer(sid, segments[1]);
|
||||||
_getUpTime(spi, segments[3]);
|
_getUpTime(sid, segments[3]);
|
||||||
_getDisk(spi, segments[5]);
|
_getDisk(sid, segments[5]);
|
||||||
_getTcp(spi, segments[4]);
|
_getTcp(sid, segments[4]);
|
||||||
_getNetSpeed(spi, segments[0]);
|
_getNetSpeed(sid, segments[0]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
s.cs = ServerConnectionState.failed;
|
s.cs = ServerState.failed;
|
||||||
s.status.failedInfo = e.toString();
|
s.status.failedInfo = e.toString();
|
||||||
logger.warning(e);
|
_logger.warning(e);
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getNetSpeed(ServerPrivateInfo spi, String raw) async {
|
Future<void> _getNetSpeed(String id, String raw) async {
|
||||||
final info = _servers.firstWhere((e) => e.spi == spi);
|
final net = await compute(parseNetSpeed, raw);
|
||||||
info.status.netSpeed.update(await compute(parseNetSpeed, raw));
|
getServer(id).status.netSpeed.update(net);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _getSysVer(ServerPrivateInfo spi, String raw) {
|
void _getSysVer(String id, String raw) {
|
||||||
final info = _servers.firstWhere((e) => e.spi == spi);
|
|
||||||
final s = raw.split('=');
|
final s = raw.split('=');
|
||||||
if (s.length == 2) {
|
if (s.length == 2) {
|
||||||
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
|
final ver = s[1].replaceAll('"', '').replaceFirst('\n', '');
|
||||||
|
getServer(id).status.sysVer = ver;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getCPU(ServerPrivateInfo spi, String raw, String tempType,
|
Future<void> _getCPU(
|
||||||
String tempValue) async {
|
String id, String raw, String tempType, String tempValue) async {
|
||||||
final info = _servers.firstWhere((e) => e.spi == spi);
|
|
||||||
final cpus = await compute(parseCPU, raw);
|
final cpus = await compute(parseCPU, raw);
|
||||||
|
final temp = await compute(parseCPUTemp, [tempType, tempValue]);
|
||||||
|
|
||||||
if (cpus.isNotEmpty) {
|
if (cpus.isNotEmpty) {
|
||||||
info.status.cpu.update(
|
getServer(id).status.cpu.update(cpus, temp);
|
||||||
cpus,
|
|
||||||
await compute(parseCPUTemp, [tempType, tempValue]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _getUpTime(ServerPrivateInfo spi, String raw) {
|
void _getUpTime(String id, String raw) {
|
||||||
_servers.firstWhere((e) => e.spi == spi).status.uptime =
|
getServer(id).status.uptime = raw.split('up ')[1].split(', ')[0];
|
||||||
raw.split('up ')[1].split(', ')[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getTcp(ServerPrivateInfo spi, String raw) async {
|
Future<void> _getTcp(String id, String raw) async {
|
||||||
final info = _servers.firstWhere((e) => e.spi == spi);
|
|
||||||
final status = await compute(parseTcp, raw);
|
final status = await compute(parseTcp, raw);
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
info.status.tcp = status;
|
getServer(id).status.tcp = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getDisk(ServerPrivateInfo spi, String raw) async {
|
Future<void> _getDisk(String id, String raw) async {
|
||||||
final info = _servers.firstWhere((e) => e.spi == spi);
|
getServer(id).status.disk = await compute(parseDisk, raw);
|
||||||
info.status.disk = await compute(parseDisk, raw);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getMem(ServerPrivateInfo spi, String raw) async {
|
Future<void> _getMem(String id, String raw) async {
|
||||||
final info = _servers.firstWhere((e) => e.spi == spi);
|
getServer(id).status.mem = await compute(parseMem, raw);
|
||||||
info.status.mem = await compute(parseMem, raw);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> runSnippet(String id, Snippet snippet) async {
|
Future<String?> runSnippet(String id, Snippet snippet) async {
|
||||||
final client =
|
final client = getServer(id).client;
|
||||||
_servers.firstWhere((element) => element.spi.id == id).client;
|
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -249,8 +257,12 @@ class _TryLimiter {
|
|||||||
final Map<String, int> _triedTimes = {};
|
final Map<String, int> _triedTimes = {};
|
||||||
|
|
||||||
bool shouldTry(String id) {
|
bool shouldTry(String id) {
|
||||||
|
final maxCount = locator<SettingStore>().maxRetryCount.fetch()!;
|
||||||
|
if (maxCount <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
final times = _triedTimes[id] ?? 0;
|
final times = _triedTimes[id] ?? 0;
|
||||||
if (times >= serverMaxTryTimes) {
|
if (times >= maxCount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_triedTimes[id] = times + 1;
|
_triedTimes[id] = times + 1;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
class BuildData {
|
class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 213;
|
static const int build = 214;
|
||||||
static const String engine =
|
static const String engine =
|
||||||
"Flutter 3.7.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b06b8b2710 (9 days ago) • 2023-01-23 16:55:55 -0800\nEngine • revision b24591ed32\nTools • Dart 2.19.0 • DevTools 2.20.1\n";
|
"Flutter 3.7.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b06b8b2710 (10 days ago) • 2023-01-23 16:55:55 -0800\nEngine • revision b24591ed32\nTools • Dart 2.19.0 • DevTools 2.20.1\n";
|
||||||
static const String buildAt = "2023-02-02 16:57:16.480921";
|
static const String buildAt = "2023-02-03 12:48:35.264858";
|
||||||
static const int modifications = 5;
|
static const int modifications = 12;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
const serverMaxTryTimes = 7;
|
|
||||||
|
|
||||||
final numReg = RegExp(r'\s{1,}');
|
final numReg = RegExp(r'\s{1,}');
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ class ServerStore extends PersistentStore {
|
|||||||
return ss;
|
return ss;
|
||||||
}
|
}
|
||||||
|
|
||||||
void delete(ServerPrivateInfo s) {
|
void delete(String id) {
|
||||||
box.delete(s.id);
|
box.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
|
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
|
||||||
if (!have(old)) {
|
if (!have(old)) {
|
||||||
throw Exception('Old ServerPrivateInfo not found');
|
throw Exception('Old ServerPrivateInfo not found');
|
||||||
}
|
}
|
||||||
|
delete(old.id);
|
||||||
put(newInfo);
|
put(newInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,7 @@ class SettingStore extends PersistentStore {
|
|||||||
|
|
||||||
StoreProperty<int> get termColorIdx =>
|
StoreProperty<int> get termColorIdx =>
|
||||||
property('termColorIdx', defaultValue: 0);
|
property('termColorIdx', defaultValue: 0);
|
||||||
|
|
||||||
|
/// Max retry count when connect to server
|
||||||
|
StoreProperty<int> get maxRetryCount => property('maxRetryCount', defaultValue: 7);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"loss": MessageLookupByLibrary.simpleMessage("loss"),
|
"loss": MessageLookupByLibrary.simpleMessage("loss"),
|
||||||
"madeWithLove": m8,
|
"madeWithLove": m8,
|
||||||
"max": MessageLookupByLibrary.simpleMessage("max"),
|
"max": MessageLookupByLibrary.simpleMessage("max"),
|
||||||
|
"maxRetryCount": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Number of server reconnection"),
|
||||||
|
"maxRetryCountEqual0":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Will retry again and again."),
|
||||||
"min": MessageLookupByLibrary.simpleMessage("min"),
|
"min": MessageLookupByLibrary.simpleMessage("min"),
|
||||||
"ms": MessageLookupByLibrary.simpleMessage("ms"),
|
"ms": MessageLookupByLibrary.simpleMessage("ms"),
|
||||||
"name": MessageLookupByLibrary.simpleMessage("Name"),
|
"name": MessageLookupByLibrary.simpleMessage("Name"),
|
||||||
@@ -243,6 +247,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"Are you sure to use no password?"),
|
"Are you sure to use no password?"),
|
||||||
"sureToDeleteServer": m14,
|
"sureToDeleteServer": m14,
|
||||||
"termTheme": MessageLookupByLibrary.simpleMessage("Terminal theme"),
|
"termTheme": MessageLookupByLibrary.simpleMessage("Terminal theme"),
|
||||||
|
"times": MessageLookupByLibrary.simpleMessage("Times"),
|
||||||
"ttl": MessageLookupByLibrary.simpleMessage("ttl"),
|
"ttl": MessageLookupByLibrary.simpleMessage("ttl"),
|
||||||
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
|
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
|
||||||
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
|
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
|
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
|
||||||
"madeWithLove": m8,
|
"madeWithLove": m8,
|
||||||
"max": MessageLookupByLibrary.simpleMessage("最大"),
|
"max": MessageLookupByLibrary.simpleMessage("最大"),
|
||||||
|
"maxRetryCount": MessageLookupByLibrary.simpleMessage("连接服务器重试次数"),
|
||||||
|
"maxRetryCountEqual0": MessageLookupByLibrary.simpleMessage("会无限重试"),
|
||||||
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
||||||
"ms": MessageLookupByLibrary.simpleMessage("毫秒"),
|
"ms": MessageLookupByLibrary.simpleMessage("毫秒"),
|
||||||
"name": MessageLookupByLibrary.simpleMessage("名称"),
|
"name": MessageLookupByLibrary.simpleMessage("名称"),
|
||||||
@@ -210,6 +212,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"sureNoPwd": MessageLookupByLibrary.simpleMessage("确认使用无密码?"),
|
"sureNoPwd": MessageLookupByLibrary.simpleMessage("确认使用无密码?"),
|
||||||
"sureToDeleteServer": m14,
|
"sureToDeleteServer": m14,
|
||||||
"termTheme": MessageLookupByLibrary.simpleMessage("终端主题"),
|
"termTheme": MessageLookupByLibrary.simpleMessage("终端主题"),
|
||||||
|
"times": MessageLookupByLibrary.simpleMessage("次"),
|
||||||
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
|
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
|
||||||
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
||||||
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
|
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
|
||||||
|
|||||||
@@ -821,6 +821,26 @@ class S {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Number of server reconnection`
|
||||||
|
String get maxRetryCount {
|
||||||
|
return Intl.message(
|
||||||
|
'Number of server reconnection',
|
||||||
|
name: 'maxRetryCount',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Will retry again and again.`
|
||||||
|
String get maxRetryCountEqual0 {
|
||||||
|
return Intl.message(
|
||||||
|
'Will retry again and again.',
|
||||||
|
name: 'maxRetryCountEqual0',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `min`
|
/// `min`
|
||||||
String get min {
|
String get min {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
@@ -1391,6 +1411,16 @@ class S {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Times`
|
||||||
|
String get times {
|
||||||
|
return Intl.message(
|
||||||
|
'Times',
|
||||||
|
name: 'times',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `ttl`
|
/// `ttl`
|
||||||
String get ttl {
|
String get ttl {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
"loss": "loss",
|
"loss": "loss",
|
||||||
"madeWithLove": "\nMade with ❤️ by {myGithub}",
|
"madeWithLove": "\nMade with ❤️ by {myGithub}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
|
"maxRetryCount": "Number of server reconnection",
|
||||||
|
"maxRetryCountEqual0": "Will retry again and again.",
|
||||||
"min": "min",
|
"min": "min",
|
||||||
"ms": "ms",
|
"ms": "ms",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@@ -133,6 +135,7 @@
|
|||||||
"sureNoPwd": "Are you sure to use no password?",
|
"sureNoPwd": "Are you sure to use no password?",
|
||||||
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
|
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
|
||||||
"termTheme": "Terminal theme",
|
"termTheme": "Terminal theme",
|
||||||
|
"times": "Times",
|
||||||
"ttl": "ttl",
|
"ttl": "ttl",
|
||||||
"unknown": "unknown",
|
"unknown": "unknown",
|
||||||
"unknownError": "Unknown error",
|
"unknownError": "Unknown error",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
"loss": "丢包率",
|
"loss": "丢包率",
|
||||||
"madeWithLove": "\n用❤️制作 by {myGithub}",
|
"madeWithLove": "\n用❤️制作 by {myGithub}",
|
||||||
"max": "最大",
|
"max": "最大",
|
||||||
|
"maxRetryCount": "连接服务器重试次数",
|
||||||
|
"maxRetryCountEqual0": "会无限重试",
|
||||||
"min": "最小",
|
"min": "最小",
|
||||||
"ms": "毫秒",
|
"ms": "毫秒",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
@@ -133,6 +135,7 @@
|
|||||||
"sureNoPwd": "确认使用无密码?",
|
"sureNoPwd": "确认使用无密码?",
|
||||||
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
|
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
|
||||||
"termTheme": "终端主题",
|
"termTheme": "终端主题",
|
||||||
|
"times": "次",
|
||||||
"ttl": "缓存时间",
|
"ttl": "缓存时间",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"unknownError": "未知错误",
|
"unknownError": "未知错误",
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
'${ns.speedIn(device: device)} | ${ns.totalIn(device: device)}',
|
'${ns.speedIn(device: device)} | ${ns.totalIn(device: device)}',
|
||||||
style: textSize11,
|
style: textSize11,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
textScaleFactor: 0.87,
|
textScaleFactor: 0.9,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -354,7 +354,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
'${ns.speedOut(device: device)} | ${ns.totalOut(device: device)}',
|
'${ns.speedOut(device: device)} | ${ns.totalOut(device: device)}',
|
||||||
style: textSize11,
|
style: textSize11,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
textScaleFactor: 0.87,
|
textScaleFactor: 0.9,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
[
|
[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_serverProvider.delServer(widget.spi!);
|
_serverProvider.delServer(widget.spi!.id);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -123,13 +123,12 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRealServerCard(ServerStatus ss, String serverName,
|
Widget _buildRealServerCard(ServerStatus ss, String serverName,
|
||||||
ServerConnectionState cs, ServerPrivateInfo spi) {
|
ServerState cs, ServerPrivateInfo spi) {
|
||||||
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
|
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
|
||||||
|
|
||||||
final topRightStr =
|
final topRightStr =
|
||||||
getTopRightStr(cs, ss.cpu.temp, ss.uptime, ss.failedInfo);
|
getTopRightStr(cs, ss.cpu.temp, ss.uptime, ss.failedInfo);
|
||||||
final hasError =
|
final hasError = cs == ServerState.failed && ss.failedInfo != null;
|
||||||
cs == ServerConnectionState.failed && ss.failedInfo != null;
|
|
||||||
final style = TextStyle(
|
final style = TextStyle(
|
||||||
color: _theme.textTheme.bodyLarge!.color!.withAlpha(100), fontSize: 11);
|
color: _theme.textTheme.bodyLarge!.color!.withAlpha(100), fontSize: 11);
|
||||||
|
|
||||||
@@ -288,12 +287,12 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getTopRightStr(ServerConnectionState cs, String temp, String upTime,
|
String getTopRightStr(
|
||||||
String? failedInfo) {
|
ServerState cs, String temp, String upTime, String? failedInfo) {
|
||||||
switch (cs) {
|
switch (cs) {
|
||||||
case ServerConnectionState.disconnected:
|
case ServerState.disconnected:
|
||||||
return _s.disconnected;
|
return _s.disconnected;
|
||||||
case ServerConnectionState.connected:
|
case ServerState.connected:
|
||||||
if (temp == '') {
|
if (temp == '') {
|
||||||
if (upTime == '') {
|
if (upTime == '') {
|
||||||
return _s.serverTabLoading;
|
return _s.serverTabLoading;
|
||||||
@@ -307,9 +306,9 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
return '$temp | $upTime';
|
return '$temp | $upTime';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ServerConnectionState.connecting:
|
case ServerState.connecting:
|
||||||
return _s.serverTabConnecting;
|
return _s.serverTabConnecting;
|
||||||
case ServerConnectionState.failed:
|
case ServerState.failed:
|
||||||
if (failedInfo == null) {
|
if (failedInfo == null) {
|
||||||
return _s.serverTabFailed;
|
return _s.serverTabFailed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
late int _selectedColorValue;
|
late int _selectedColorValue;
|
||||||
late int _launchPageIdx;
|
late int _launchPageIdx;
|
||||||
late int _termThemeIdx;
|
late int _termThemeIdx;
|
||||||
|
late double _maxRetryCount;
|
||||||
late double _updateInterval;
|
late double _updateInterval;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,6 +50,7 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
_launchPageIdx = _setting.launchPage.fetch()!;
|
_launchPageIdx = _setting.launchPage.fetch()!;
|
||||||
_termThemeIdx = _setting.termColorIdx.fetch()!;
|
_termThemeIdx = _setting.termColorIdx.fetch()!;
|
||||||
_updateInterval = _setting.serverStatusUpdateInterval.fetch()!.toDouble();
|
_updateInterval = _setting.serverStatusUpdateInterval.fetch()!.toDouble();
|
||||||
|
_maxRetryCount = _setting.maxRetryCount.fetch()!.toDouble();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -66,6 +68,7 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
_buildLaunchPage(),
|
_buildLaunchPage(),
|
||||||
_buildDistLogoSwitch(),
|
_buildDistLogoSwitch(),
|
||||||
_buildTermTheme(),
|
_buildTermTheme(),
|
||||||
|
_buildMaxRetry(),
|
||||||
].map((e) => RoundRectCard(e)).toList(),
|
].map((e) => RoundRectCard(e)).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -286,4 +289,48 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMaxRetry() {
|
||||||
|
return ExpansionTile(
|
||||||
|
textColor: primaryColor,
|
||||||
|
title: Text(
|
||||||
|
_s.maxRetryCount,
|
||||||
|
style: textSize13,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
trailing: Text('${_maxRetryCount.toInt()} ${_s.times}'),
|
||||||
|
children: [
|
||||||
|
Slider(
|
||||||
|
thumbColor: primaryColor,
|
||||||
|
activeColor: primaryColor.withOpacity(0.7),
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
value: _maxRetryCount,
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_maxRetryCount = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (val) {
|
||||||
|
_setting.maxRetryCount.put(val.toInt());
|
||||||
|
},
|
||||||
|
label: '${_maxRetryCount.toInt()} ${_s.times}',
|
||||||
|
divisions: 10,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 3,
|
||||||
|
),
|
||||||
|
_maxRetryCount == 0.0
|
||||||
|
? Text(
|
||||||
|
_s.maxRetryCountEqual0,
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 13,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildFileView() {
|
Widget _buildFileView() {
|
||||||
if (_client == null || _si?.cs != ServerConnectionState.connected) {
|
if (_client == null || _si?.cs != ServerState.connected) {
|
||||||
return centerCircleLoading;
|
return centerCircleLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,14 +105,18 @@ class _SSHPageState extends State<SSHPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final termTheme = _isDark ? termDarkTheme : termLightTheme;
|
final termTheme = _isDark ? termDarkTheme : termLightTheme;
|
||||||
return AnnotatedRegion(
|
Widget child = Scaffold(
|
||||||
value: _isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,
|
backgroundColor: termTheme.background,
|
||||||
child: Scaffold(
|
body: _buildBody(termTheme.toTerminalTheme(_termColors)),
|
||||||
backgroundColor: termTheme.background,
|
bottomNavigationBar: _buildBottom(termTheme.background),
|
||||||
body: _buildBody(termTheme.toTerminalTheme(_termColors)),
|
|
||||||
bottomNavigationBar: _buildBottom(termTheme.background),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
child = AnnotatedRegion(
|
||||||
|
value: _isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(TerminalTheme termTheme) {
|
Widget _buildBody(TerminalTheme termTheme) {
|
||||||
|
|||||||
Reference in New Issue
Block a user