Compare commits

...

23 Commits

Author SHA1 Message Date
Junyuan Feng
bfd31e561c optimize ping page & make.dart 2022-11-07 20:15:39 +08:00
Junyuan Feng
5b8468effa rm dep. menu & optimize 2022-11-07 19:54:26 +08:00
Junyuan Feng
bd04fd3f1a rm dep. dropdownbtn 2022-11-07 19:12:17 +08:00
Junyuan Feng
398c49bb99 使用 Hive Object 2022-11-05 23:08:56 +08:00
Junyuan Feng
c036b78708 增加 armbian 头图 2022-11-05 21:58:05 +08:00
Junyuan Feng
7f1bd06422 Optimzed & Flutter 3.3.4 2022-10-21 17:50:13 +08:00
Junyuan Feng
06302d7162 fix
- auto get user id for rootless docker
2022-06-05 11:20:35 +08:00
Junyuan Feng
57ecdddc76 update readme 2022-05-31 11:54:09 +08:00
Junyuan Feng
04db5a82b9 fix docker parse error
- rootless docker socket host
- version & edition
2022-05-31 11:02:59 +08:00
Junyuan Feng
bb1bf9219c feat & fix
- support openwrt
- save server info failed if connect error
- support ssh 'none' auth
2022-05-29 18:34:55 +08:00
Junyuan Feng
3ed476275f update readme 2022-05-25 13:51:37 +08:00
Junyuan Feng
9cbd29d3c1 fix
- range error when no upgradeable
- remove auto input pwd when apt pwd request
2022-05-25 13:29:51 +08:00
Junyuan Feng
8cbe76d9ff add thanks 2022-05-25 13:18:51 +08:00
Junyuan Feng
1bd673a164 fix #3
- server edit page display bugs, include userPubKey and pubKeyIdx
2022-05-25 13:10:30 +08:00
Junyuan Feng
4148aac31b add feedback 2022-05-24 13:43:54 +08:00
Junyuan Feng
64196a7ba6 fix & opt
- display when client.run no result
- remove dep. marquee
2022-05-24 13:25:44 +08:00
Junyuan Feng
cb5aed8e79 fix & opt
- apt/yum if not root, auto try saved pwd
- ServerPrivateInfo.authorization: Object => String pwd
- fix apt parse: caused by irrelevant output
- serverprovider replace _servers[idx] with s
2022-05-24 12:44:12 +08:00
Junyuan Feng
f60b09abe4 update deps
- support private key rsa algorithm
2022-05-23 17:48:00 +08:00
Junyuan Feng
d4368f5084 feat.
- add FocusNode and onSubmitted to TextField
2022-05-23 10:59:10 +08:00
Junyuan Feng
330ff9a621 fix & opt
- fix duplicated ssh connecting
- opt backup page ui
2022-05-23 10:35:42 +08:00
Junyuan Feng
0fdc1b784b feat. & fix.
- support backup & restore
- fix when client.run empty return
2022-05-22 13:02:54 +08:00
Junyuan Feng
228f228d44 upgrade & opt
- flutter 3.0.1
- show snackbar when no server to ping
- server detail page replace nice to iowait
- make script show stdout when error
2022-05-21 20:39:52 +08:00
Junyuan Feng
b4d42eecf3 fix
- multi dialog on apt pwd request
- update dialog condition error
2022-05-20 19:51:14 +08:00
83 changed files with 1735 additions and 977 deletions

4
.fvm/fvm_config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"flutterSdkVersion": "3.3.4",
"flavors": {}
}

1
.gitignore vendored
View File

@@ -48,3 +48,4 @@ app.*.map.json
/android/app/fjy.androidstudio.key
/release
test.dart
.fvm/flutter_sdk

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"dart.flutterSdkPath": ".fvm/flutter_sdk",
"search.exclude": {
"**/.fvm": true
},
"files.watcherExclude": {
"**/.fvm": true
}
}

View File

@@ -1,67 +1,63 @@
# Server Monitor & Toolbox
A new Flutter project which provide a chart view to display server status data and a manager toolbox.
<!-- Title-->
<p align="center">
<h1 align="center">Server Box</h1>
</p>
## Milestone
<!-- Badges-->
<p align="center">
<a href="https://apps.apple.com/app/id1586449703">
<img style="height: 37px" src="screenshots/appstore.svg">
</a>
<a href="https://github.com/LollipopKit/flutter_server_box/releases/latest">
<img style="height: 37px" src="screenshots/dl-android.svg">
</a>
</p>
<p align="center">
A Flutter project which provide charts to display server status and tools to manage server. Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a>.
</p>
## 🔖 Milestone
- [x] Status chart view
- [x] Base64/Url En/Decode
- [x] Ping
- [x] Desktop support
- [x] Apt/Yum manager
- [x] SFTP
- [ ] Snippet market
- [x] Docker manager
- [x] Snippet ~~market~~, Ping, SFTP, Docker, Apt/Yum and etc.
- [x] i18n (English, Chinese)
- [x] Desktop support
## ScreenShots
## 📱 ScreenShots
<table>
<tr>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server.jpg">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_detail.jpg">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_detail.png">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_edit.jpg">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/convert.png">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/convert.jpg">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/ping.png">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/ping.jpg">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/apt.png">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/setting.jpg">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/drawer.jpg">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/docker.png">
</td>
</tr>
</table>
# Support
## 🖥 Platform
Status|Platform
--|--|
Full Support|Android/iOS/macOS
Support, but not tested|Windows/Linux
## Build
Please use `make.dart` to build.
```shell
# build android apk and ios archive
./make.dart build
# Run in release mode
./make.dart run release
```
## License
## 📝 License
`LGPL License. LollipopKit 2021`

File diff suppressed because one or more lines are too long

BIN
assets/linux/armbian.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/linux/wrt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -40,13 +40,13 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
countly_flutter: e245f94349d8adf306c22e60c10648c69aae7380
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.3

View File

@@ -339,7 +339,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -354,7 +354,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 159;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -362,7 +362,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.136;
MARKETING_VERSION = 1.0.159;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -418,7 +418,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -467,7 +467,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -484,7 +484,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 159;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -492,7 +492,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.136;
MARKETING_VERSION = 1.0.159;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -508,7 +508,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 159;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -516,7 +516,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.136;
MARKETING_VERSION = 1.0.159;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ enum _BuildMode {
profile,
}
_BuildMode _buildMode = (() {
final _buildMode = (() {
if (const bool.fromEnvironment('dart.vm.product')) {
return _BuildMode.release;
}

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
class PersistentStore<E> {
late Box<E> box;

View File

@@ -9,11 +9,6 @@ class ProviderBase with ChangeNotifier {
}
}
enum ProviderState {
idle,
busy,
}
class BusyProvider extends ProviderBase {
bool _isBusy = false;
bool get isBusy => _isBusy;

View File

@@ -11,14 +11,14 @@ import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
final logger = Logger('UPDATE');
final _logger = Logger('UPDATE');
Future<bool> isFileAvailable(String url) async {
try {
final resp = await Dio().head(url);
return resp.statusCode == 200;
} catch (e) {
logger.warning('update file not available: $e');
_logger.warning('update file not available: $e');
return false;
}
}
@@ -40,11 +40,11 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
}();
if (!force && newest <= BuildData.build) {
logger.info('Update ignored due to current: ${BuildData.build}, '
_logger.info('Update ignored due to current: ${BuildData.build}, '
'update: $newest');
return;
}
logger.info('Update available: $newest');
_logger.info('Update available: $newest');
if (Platform.isAndroid && !await isFileAvailable(update.android)) {
return;
@@ -57,7 +57,7 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
update.min > BuildData.build
? 'Your version is too old. \nPlease update to v1.0.$newest.'
: 'Update: v1.0.$newest available. \n${update.changelog}',
'Update', () async {
s.update, () async {
if (Platform.isAndroid) {
await RUpgrade.upgrade(update.android,
fileName: update.android.split('/').last, isAutoRequestInstall: true);
@@ -65,10 +65,11 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
await RUpgrade.upgradeFromAppStore('1586449703');
} else if (Platform.isMacOS) {
await RUpgrade.upgradeFromUrl(update.mac);
} else {
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
]);
}
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
]);
});
}

View File

@@ -10,8 +10,6 @@ import 'package:toolbox/view/widget/card_dialog.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:toolbox/core/extension/stringx.dart';
void unawaited(Future<void> future) {}
bool isDarkMode(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark;
@@ -110,3 +108,21 @@ Future<bool> shareFiles(BuildContext context, List<String> filePaths) async {
await Share.shareFiles(filePaths, text: 'ServerBox -> $text');
return filePaths.isNotEmpty;
}
Widget buildPopuopMenu(
{required List<PopupMenuEntry> items,
required Function(dynamic) onSelected}) {
return PopupMenuButton(
itemBuilder: (_) => items,
onSelected: onSelected,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(
Icons.more_vert,
size: 21,
),
),
);
}

View File

@@ -0,0 +1,52 @@
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/snippet.dart';
class Backup {
// backup format version
final int version;
final String date;
final List<ServerPrivateInfo> spis;
final List<Snippet> snippets;
final List<PrivateKeyInfo> keys;
final int primaryColor;
final int serverStatusUpdateInterval;
final int launchPage;
Backup(
this.version,
this.date,
this.spis,
this.snippets,
this.keys,
this.primaryColor,
this.serverStatusUpdateInterval,
this.launchPage,
);
Backup.fromJson(Map<String, dynamic> json)
: version = json['version'] as int,
date = json['date'],
spis = (json['spis'] as List)
.map((e) => ServerPrivateInfo.fromJson(e))
.toList(),
snippets =
(json['snippets'] as List).map((e) => Snippet.fromJson(e)).toList(),
keys = (json['keys'] as List)
.map((e) => PrivateKeyInfo.fromJson(e))
.toList(),
primaryColor = json['primaryColor'],
serverStatusUpdateInterval = json['serverStatusUpdateInterval'],
launchPage = json['launchPage'];
Map<String, dynamic> toJson() => {
'version': version,
'date': date,
'spis': spis,
'snippets': snippets,
'keys': keys,
'primaryColor': primaryColor,
'serverStatusUpdateInterval': serverStatusUpdateInterval,
'launchPage': launchPage,
};
}

View File

@@ -9,8 +9,10 @@ const debianDistList = [
'ubuntu',
'linuxmint',
'elementary',
'raspbian'
'raspbian',
'armbian'
];
const rehlDistList = [
'redhat',
'fedora',

View File

@@ -1,46 +1,53 @@
import 'package:toolbox/data/model/server/cpu_status.dart';
class Cpu2Status {
List<CpuStatus> pre;
List<CpuStatus> now;
List<CpuStatus> _pre;
List<CpuStatus> _now;
String temp;
Cpu2Status(this.pre, this.now, this.temp);
Cpu2Status(this._pre, this._now, this.temp);
double usedPercent({int coreIdx = 0}) {
if (now.length != pre.length) return 0;
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
if (_now.length != _pre.length) return 0;
final idleDelta = _now[coreIdx].idle - _pre[coreIdx].idle;
final totalDelta = _now[coreIdx].total - _pre[coreIdx].total;
final used = idleDelta / totalDelta;
return used.isNaN ? 0 : 100 - used * 100;
}
void update(List<CpuStatus> newStatus, String newTemp) {
pre = now;
now = newStatus;
_pre = _now;
_now = newStatus;
temp = newTemp;
}
int get coresCount => now.length;
int get coresCount => _now.length;
int get totalDelta => now[0].total - pre[0].total;
int get totalDelta => _now[0].total - _pre[0].total;
double get user {
if (now.length != pre.length) return 0;
final delta = now[0].user - pre[0].user;
if (_now.length != _pre.length) return 0;
final delta = _now[0].user - _pre[0].user;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get sys {
if (now.length != pre.length) return 0;
final delta = now[0].sys - pre[0].sys;
if (_now.length != _pre.length) return 0;
final delta = _now[0].sys - _pre[0].sys;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get nice {
if (now.length != pre.length) return 0;
final delta = now[0].nice - pre[0].nice;
if (_now.length != _pre.length) return 0;
final delta = _now[0].nice - _pre[0].nice;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}
double get iowait {
if (_now.length != _pre.length) return 0;
final delta = _now[0].iowait - _pre[0].iowait;
final used = delta / totalDelta;
return used.isNaN ? 0 : used * 100;
}

View File

@@ -1,14 +0,0 @@
class LinuxIcons {
List<String> db;
LinuxIcons(this.db);
String? search(String sysVer) {
for (var item in db) {
if (sysVer.toLowerCase().contains(item)) {
return 'assets/linux/$item.png';
}
}
return null;
}
}

View File

@@ -2,14 +2,12 @@ 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

@@ -9,46 +9,46 @@ class NetSpeedPart {
}
class NetSpeed {
List<NetSpeedPart> old;
List<NetSpeedPart> now;
NetSpeed(this.old, this.now);
List<NetSpeedPart> _old;
List<NetSpeedPart> _now;
NetSpeed(this._old, this._now);
List<String> get devices {
final devices = <String>[];
for (var item in now) {
for (var item in _now) {
devices.add(item.device);
}
return devices;
}
void update(List<NetSpeedPart> newOne) {
old = now;
now = newOne;
_old = _now;
_now = newOne;
}
int get timeDiff => now[0].time - old[0].time;
int get timeDiff => _now[0].time - _old[0].time;
String speedIn({String? device}) {
if (old[0].device == '' || now[0].device == '') return '0kb/s';
if (_old[0].device == '' || _now[0].device == '') return '0kb/s';
final idx = deviceIdx(device);
final speedInBytesPerSecond =
(now[idx].bytesIn - old[idx].bytesIn) / timeDiff;
(_now[idx].bytesIn - _old[idx].bytesIn) / timeDiff;
return buildStandardOutput(speedInBytesPerSecond);
}
String speedOut({String? device}) {
if (old[0].device == '' || now[0].device == '') return '0kb/s';
if (_old[0].device == '' || _now[0].device == '') return '0kb/s';
final idx = deviceIdx(device);
final speedInBytesPerSecond =
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
return buildStandardOutput(speedInBytesPerSecond);
final speedOutBytesPerSecond =
(_now[idx].bytesOut - _old[idx].bytesOut) / timeDiff;
return buildStandardOutput(speedOutBytesPerSecond);
}
int deviceIdx(String? device) {
if (device != null) {
for (var item in now) {
for (var item in _now) {
if (item.device == device) {
return now.indexOf(item);
return _now.indexOf(item);
}
}
}

View File

@@ -1,8 +1,13 @@
import 'dart:convert';
import 'package:hive_flutter/hive_flutter.dart';
part 'private_key_info.g.dart';
///
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
///
@HiveType(typeId: 1)
class PrivateKeyInfo {
/*
{
@@ -12,8 +17,11 @@ class PrivateKeyInfo {
}
*/
@HiveField(0)
late String id;
@HiveField(1)
late String privateKey;
@HiveField(2)
late String password;
PrivateKeyInfo(

View File

@@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'private_key_info.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PrivateKeyInfoAdapter extends TypeAdapter<PrivateKeyInfo> {
@override
final int typeId = 1;
@override
PrivateKeyInfo read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PrivateKeyInfo(
fields[0] as String,
fields[1] as String,
fields[2] as String,
);
}
@override
void write(BinaryWriter writer, PrivateKeyInfo obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.privateKey)
..writeByte(2)
..write(obj.password);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PrivateKeyInfoAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,5 +1,4 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
@@ -11,3 +10,5 @@ class ServerInfo {
ServerInfo(this.info, this.status, this.client, this.connectionState);
}
enum ServerConnectionState { disconnected, connecting, connected, failed }

View File

@@ -1 +0,0 @@
enum ServerConnectionState { disconnected, connecting, connected, failed }

View File

@@ -1,8 +1,13 @@
import 'dart:convert';
import 'package:hive_flutter/hive_flutter.dart';
part 'server_private_info.g.dart';
///
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
///
@HiveType(typeId: 3)
class ServerPrivateInfo {
/*
{
@@ -13,26 +18,34 @@ class ServerPrivateInfo {
}
*/
@HiveField(0)
late String name;
@HiveField(1)
late String ip;
@HiveField(2)
late int port;
@HiveField(3)
late String user;
late Object authorization;
@HiveField(4)
late String pwd;
@HiveField(5)
String? pubKeyId;
String get id => '$user@$ip:$port';
ServerPrivateInfo(
{required this.name,
required this.ip,
required this.port,
required this.user,
required this.authorization,
required this.pwd,
this.pubKeyId});
ServerPrivateInfo.fromJson(Map<String, dynamic> json) {
name = json["name"].toString();
ip = json["ip"].toString();
port = json["port"].toInt();
user = json["user"].toString();
authorization = json["authorization"];
pwd = json["authorization"].toString();
pubKeyId = json["pubKeyId"]?.toString();
}
Map<String, dynamic> toJson() {
@@ -41,7 +54,7 @@ class ServerPrivateInfo {
data["ip"] = ip;
data["port"] = port;
data["user"] = user;
data["authorization"] = authorization;
data["authorization"] = pwd;
data["pubKeyId"] = pubKeyId;
return data;
}

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'server_private_info.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
@override
final int typeId = 3;
@override
ServerPrivateInfo read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerPrivateInfo(
name: fields[0] as String,
ip: fields[1] as String,
port: fields[2] as int,
user: fields[3] as String,
pwd: fields[4] as String,
pubKeyId: fields[5] as String?,
);
}
@override
void write(BinaryWriter writer, ServerPrivateInfo obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.ip)
..writeByte(2)
..write(obj.port)
..writeByte(3)
..write(obj.user)
..writeByte(4)
..write(obj.pwd)
..writeByte(5)
..write(obj.pubKeyId);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerPrivateInfoAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,7 +1,14 @@
import 'dart:convert';
import 'package:hive_flutter/hive_flutter.dart';
part 'snippet.g.dart';
@HiveType(typeId: 2)
class Snippet {
@HiveField(0)
late String name;
@HiveField(1)
late String script;
Snippet(this.name, this.script);

View File

@@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'snippet.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SnippetAdapter extends TypeAdapter<Snippet> {
@override
final int typeId = 2;
@override
Snippet read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Snippet(
fields[0] as String,
fields[1] as String,
);
}
@override
void write(BinaryWriter writer, Snippet obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.script);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SnippetAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -58,8 +58,7 @@ class SftpDownloadWorker {
SSHClient client;
if (spi.pubKeyId == null) {
client = SSHClient(socket,
username: spi.user,
onPasswordRequest: () => spi.authorization as String);
username: spi.user, onPasswordRequest: () => spi.pwd);
} else {
client = SSHClient(socket,
username: spi.user,

View File

@@ -1,20 +1,9 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/service/app.dart';
class AppProvider extends BusyProvider {
Map? _notify;
Map? get notify => _notify;
int? _newestBuild;
int? get newestBuild => _newestBuild;
Future<void> loadData() async {
setBusyState(true);
final service = AppService();
_notify = await service.getNotify();
setBusyState(false);
notifyListeners();
}
void setNewestBuild(int build) {
_newestBuild = build;
notifyListeners();

View File

@@ -11,7 +11,7 @@ import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
import 'package:toolbox/data/model/distribution.dart';
typedef PwdRequestFunc = Future<String> Function(
bool lastTimes, String? userName);
int triedTimes, String? userName);
final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):');
class AptProvider extends BusyProvider {
@@ -30,6 +30,7 @@ class AptProvider extends BusyProvider {
String? updateLog;
String lastLog = '';
int triedTimes = 0;
bool isRequestingPwd = false;
AptProvider();
@@ -55,6 +56,7 @@ class AptProvider extends BusyProvider {
onUpdate = null;
onPasswordRequest = null;
triedTimes = 0;
isRequestingPwd = false;
}
Future<void> refreshInstalled() async {
@@ -64,9 +66,8 @@ class AptProvider extends BusyProvider {
}
final result = await _update();
try {
getUpgradeableList(result);
} catch (e) {
getUpgradeableList(result);
try {} catch (e) {
error = '[Server Raw]:\n$result\n[App Error]:\n$e';
} finally {
notifyListeners();
@@ -85,7 +86,15 @@ class AptProvider extends BusyProvider {
list = list.sublist(0, endLine);
break;
default:
list = list.sublist(4);
// avoid other outputs
// such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...]
final idx =
list.indexWhere((element) => element.contains('[upgradable from:'));
if (idx == -1) {
upgradeable = [];
return;
}
list = list.sublist(idx);
list.removeWhere((element) => element.isEmpty);
}
upgradeable = list.map((e) => UpgradePkgInfo(e, dist!)).toList();
@@ -144,19 +153,22 @@ class AptProvider extends BusyProvider {
}
Future<void> _onPwd(Uint8List e, StreamSink<Uint8List> stdin) async {
if (isRequestingPwd) return;
isRequestingPwd = true;
final event = e.string;
if (event.contains('[sudo] password for ')) {
final user = pwdRequestWithUserReg.firstMatch(event)?.group(1);
logger.info('sudo password request for $user');
triedTimes++;
final pwd =
await (onPasswordRequest ?? (_) async => '')(triedTimes == 3, user);
await (onPasswordRequest ?? (_, __) async => '')(triedTimes, user);
if (pwd.isEmpty) {
logger.info('sudo password request cancelled');
return;
}
stdin.add(Uint8List.fromList(utf8.encode('$pwd\n')));
}
isRequestingPwd = false;
}
String _wrap(String cmd) =>

View File

@@ -4,19 +4,27 @@ import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/docker/ps.dart';
final dockerNotFound = RegExp(r'command not found|Unknown command');
final _dockerNotFound = RegExp(r'command not found|Unknown command');
final _versionReg = RegExp(r'(Version:)\s+([0-9]+\.[0-9]+\.[0-9]+)');
final _editionReg = RegExp(r'(Client:)\s+(.+-.+)');
final _userIdReg = RegExp(r'.+:(\d+:\d+):.+');
class DockerProvider extends BusyProvider {
SSHClient? client;
String? userName;
List<DockerPsItem>? items;
String? version;
String? edition;
String? error;
void init(SSHClient client) => this.client = client;
void init(SSHClient client, String userName) {
this.client = client;
this.userName = userName;
}
void clear() {
client = null;
userName = null;
error = null;
items = null;
version = null;
@@ -31,32 +39,33 @@ class DockerProvider extends BusyProvider {
}
final verRaw = await client!.run('docker version'.withLangExport).string;
if (verRaw.contains(dockerNotFound)) {
if (verRaw.contains(_dockerNotFound)) {
error = 'docker not found';
notifyListeners();
return;
}
final verSplit = verRaw.split('\n');
if (verSplit.length < 3) {
error = 'invalid version';
notifyListeners();
return;
} else {
try {
version = verSplit[1].split(' ').last;
edition = verSplit[0].split(': ')[1];
} catch (e) {
error = e.toString();
return;
}
}
final raw = await client!.run('docker ps -a'.withLangExport).string;
final lines = raw.split('\n');
lines.removeAt(0);
lines.removeWhere((element) => element.isEmpty);
version = _versionReg.firstMatch(verRaw)?.group(2);
edition = _editionReg.firstMatch(verRaw)?.group(2);
final passwd = await client!.run('cat /etc/passwd | grep $userName').string;
final userId = _userIdReg.firstMatch(passwd)?.group(1)?.split(':').first;
try {
final cmd = 'docker ps -a'.withLangExport;
final raw = await () async {
final raw = await client!.run(cmd).string;
if (raw.contains('permission denied')) {
return await client!
.run(
'export DOCKER_HOST=unix:///run/user/${userId ?? 1000}/docker.sock && $cmd')
.string;
}
return raw;
}();
final lines = raw.split('\n');
lines.removeAt(0);
lines.removeWhere((element) => element.isEmpty);
items = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
} catch (e) {
error = e.toString();
@@ -66,42 +75,22 @@ class DockerProvider extends BusyProvider {
}
}
Future<bool> stop(String id) async {
Future<bool> _do(String id, String cmd) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker stop $id').string;
final result = await client!.run(cmd).string;
await refresh();
setBusyState(false);
return result.contains(id);
}
Future<bool> start(String id) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker start $id').string;
await refresh();
setBusyState(false);
return result.contains(id);
}
Future<bool> stop(String id) async => await _do(id, 'docker stop $id');
Future<bool> delete(String id) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker rm $id').string;
await refresh();
setBusyState(false);
return result.contains(id);
}
Future<bool> start(String id) async => await _do(id, 'docker start $id');
Future<bool> delete(String id) async => await _do(id, 'docker rm $id');
}

View File

@@ -26,7 +26,7 @@ class PrivateKeyProvider extends BusyProvider {
void updateInfo(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final idx = _infos.indexWhere((e) => e.id == old.id);
_infos[idx] = newInfo;
locator<PrivateKeyStore>().update(old, newInfo);
locator<PrivateKeyStore>().put(newInfo);
notifyListeners();
}
}

View File

@@ -10,7 +10,6 @@ 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/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/disk_info.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
@@ -37,12 +36,13 @@ const shellCmd = "export LANG=en_US.utf-8 \necho '$seperator' \n"
"uptime \necho $seperator \n"
"cat /proc/net/snmp \necho $seperator \n"
"df -h \necho $seperator \n"
"free -m \necho $seperator \n"
"cat /proc/meminfo \necho $seperator \n"
"cat /sys/class/thermal/thermal_zone*/type \necho $seperator \n"
"cat /sys/class/thermal/thermal_zone*/temp";
const shellPath = '.serverbox.sh';
const memPrefix = 'Mem:';
final cpuTempReg = RegExp('(x86_pkg_temp|cpu_thermal)');
final cpuTempReg = RegExp(r'(x86_pkg_temp|cpu_thermal)');
final numReg = RegExp(r'\s{1,}');
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
class ServerProvider extends BusyProvider {
List<ServerInfo> _servers = [];
@@ -53,7 +53,7 @@ class ServerProvider extends BusyProvider {
final logger = Logger('ServerProvider');
Memory get emptyMemory =>
Memory(total: 1, used: 0, free: 1, shared: 0, cache: 0, avail: 1);
Memory(total: 1, used: 0, free: 1, cache: 0, avail: 1);
NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0);
@@ -91,8 +91,7 @@ class ServerProvider extends BusyProvider {
final socket = await SSHSocket.connect(spi.ip, spi.port);
if (spi.pubKeyId == null) {
return SSHClient(socket,
username: spi.user,
onPasswordRequest: () => spi.authorization as String);
username: spi.user, onPasswordRequest: () => spi.pwd);
}
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
return SSHClient(socket,
@@ -156,33 +155,35 @@ class ServerProvider extends BusyProvider {
throw RangeError.index(idx, _servers);
}
_servers[idx].info = newSpi;
_servers[idx].client = await genClient(newSpi);
locator<ServerStore>().update(old, newSpi);
_servers[idx].client = await genClient(newSpi);
notifyListeners();
refreshData(spi: newSpi);
}
Future<void> _getData(ServerPrivateInfo spi) async {
final idx = _servers.indexWhere((element) => element.info == spi);
final state = _servers[idx].connectionState;
if (_servers[idx].client == null ||
state == ServerConnectionState.failed ||
final s = _servers.firstWhere((element) => element.info == spi);
final state = s.connectionState;
if (state == ServerConnectionState.failed ||
state == ServerConnectionState.disconnected) {
_servers[idx].connectionState = ServerConnectionState.connecting;
s.connectionState = ServerConnectionState.connecting;
notifyListeners();
final time1 = DateTime.now();
try {
_servers[idx].client = await genClient(spi);
s.client = await genClient(spi);
final time2 = DateTime.now();
logger.info(
'Connected to [${spi.name}] in [${time2.difference(time1).toString()}].');
_servers[idx].connectionState = ServerConnectionState.connected;
_servers[idx]
.client!
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath");
s.connectionState = ServerConnectionState.connected;
final writeResult = await s.client!
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath")
.string;
if (writeResult.isNotEmpty) {
throw Exception(writeResult);
}
} catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed;
_servers[idx].status.failedInfo = '$e ## ';
s.connectionState = ServerConnectionState.failed;
s.status.failedInfo = '$e ## ';
logger.warning(e);
} finally {
notifyListeners();
@@ -190,23 +191,30 @@ class ServerProvider extends BusyProvider {
}
// if client is null, return
final si = _servers[idx];
if (si.client == null) return;
final raw = await si.client!.run("sh $shellPath").string;
final lines = raw.split(seperator).map((e) => e.trim()).toList();
lines.removeAt(0);
if (s.client == null) return;
final raw = await s.client!.run("sh $shellPath").string;
final segments = raw.split(seperator).map((e) => e.trim()).toList();
if (raw.isEmpty || segments.length == 1) {
s.connectionState = ServerConnectionState.failed;
if (s.status.failedInfo == null || s.status.failedInfo!.isEmpty) {
s.status.failedInfo = 'No data received';
}
notifyListeners();
return;
}
segments.removeAt(0);
try {
_getCPU(spi, lines[2], lines[7], lines[8]);
_getMem(spi, lines[6]);
_getSysVer(spi, lines[1]);
_getUpTime(spi, lines[3]);
_getDisk(spi, lines[5]);
_getTcp(spi, lines[4]);
_getNetSpeed(spi, lines[0]);
_getCPU(spi, segments[2], segments[7], segments[8]);
_getMem(spi, segments[6]);
_getSysVer(spi, segments[1]);
_getUpTime(spi, segments[3]);
_getDisk(spi, segments[5]);
_getTcp(spi, segments[4]);
_getNetSpeed(spi, segments[0]);
} catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed;
servers[idx].status.failedInfo = e.toString();
s.connectionState = ServerConnectionState.failed;
s.status.failedInfo = e.toString();
logger.warning(e);
rethrow;
} finally {
@@ -220,23 +228,9 @@ class ServerProvider extends BusyProvider {
/// 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
void _getNetSpeed(ServerPrivateInfo spi, String raw) {
Future<void> _getNetSpeed(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
final split = raw.split('\n');
final deviceCount = split.length - 3;
if (deviceCount < 1) return;
final time = int.parse(split[split.length - 1]);
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));
}
info.status.netSpeed.update(results);
info.status.netSpeed.update(await compute(_parseNetSpeed, raw));
}
void _getSysVer(ServerPrivateInfo spi, String raw) {
@@ -247,47 +241,14 @@ class ServerProvider extends BusyProvider {
}
}
String _getCPUTemp(String type, String value) {
const noMatch = "/sys/class/thermal/thermal_zone*/type";
// Not support to get CPU temperature
if (value.contains(noMatch) ||
type.contains(noMatch) ||
value.isEmpty ||
type.isEmpty) {
return '';
}
final split = type.split('\n');
int idx = 0;
for (var item in split) {
if (item.contains(cpuTempReg)) {
break;
}
idx++;
}
return '${(int.parse(value.split('\n')[idx].trim()) / 1000).toStringAsFixed(1)}°C';
}
void _getCPU(
ServerPrivateInfo spi, String raw, String tempType, String tempValue) {
Future<void> _getCPU(ServerPrivateInfo spi, String raw, String tempType,
String tempValue) async {
final info = _servers.firstWhere((e) => e.info == spi);
final List<CpuStatus> cpus = [];
final cpus = await compute(_parseCPU, raw);
for (var item in raw.split('\n')) {
if (item == '') break;
final id = item.split(' ').first;
final matches = item.replaceFirst(id, '').trim().split(' ');
cpus.add(CpuStatus(
id,
int.parse(matches[0]),
int.parse(matches[1]),
int.parse(matches[2]),
int.parse(matches[3]),
int.parse(matches[4]),
int.parse(matches[5]),
int.parse(matches[6])));
}
if (cpus.isNotEmpty) {
info.status.cpu2Status.update(cpus, _getCPUTemp(tempType, tempValue));
info.status.cpu2Status
.update(cpus, await compute(_getCPUTemp, [tempType, tempValue]));
}
}
@@ -296,14 +257,11 @@ class ServerProvider extends BusyProvider {
raw.split('up ')[1].split(', ')[0];
}
void _getTcp(ServerPrivateInfo spi, String raw) {
Future<void> _getTcp(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
final lines = raw.split('\n');
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
orElse: () => '');
if (idx != '') {
final vals = idx.split(RegExp(r'\s{1,}'));
info.status.tcp = TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
final status = await compute(_parseTcp, raw);
if (status != null) {
info.status.tcp = status;
}
}
@@ -315,30 +273,17 @@ class ServerProvider extends BusyProvider {
if (items.indexOf(item) == 0 || item.isEmpty) {
continue;
}
final vals = item.split(RegExp(r'\s{1,}'));
final vals = item.split(numReg);
list.add(DiskInfo(vals[0], vals[5],
int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3]));
}
info.status.disk = list;
}
void _getMem(ServerPrivateInfo spi, String raw) {
Future<void> _getMem(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
for (var item in raw.split('\n')) {
if (item.contains(memPrefix)) {
final split = item.replaceFirst(memPrefix, '').split(' ');
split.removeWhere((e) => e == '');
final memList = split.map((e) => int.parse(e)).toList();
info.status.memory = Memory(
total: memList[0],
used: memList[1],
free: memList[2],
shared: memList[3],
cache: memList[4],
avail: memList[5]);
break;
}
}
final mem = await compute(_parseMem, raw);
info.status.memory = mem;
}
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
@@ -349,3 +294,96 @@ class ServerProvider extends BusyProvider {
.string;
}
}
Memory _parseMem(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
final total = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemTotal:')?.group(2) ?? '1');
final free = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemFree:')?.group(2) ?? '0');
final cached = int.parse(
items.firstWhere((e) => e?.group(1) == 'Cached:')?.group(2) ?? '0');
final available = int.parse(
items.firstWhere((e) => e?.group(1) == 'MemAvailable:')?.group(2) ?? '0');
return Memory(
total: total,
used: total - available,
free: free,
cache: cached,
avail: available);
}
TcpStatus? _parseTcp(String raw) {
final lines = raw.split('\n');
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
orElse: () => '');
if (idx != '') {
final vals = idx.split(numReg);
return TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
}
return null;
}
List<CpuStatus> _parseCPU(String raw) {
final List<CpuStatus> cpus = [];
for (var item in raw.split('\n')) {
if (item == '') break;
final id = item.split(' ').first;
final matches = item.replaceFirst(id, '').trim().split(' ');
cpus.add(CpuStatus(
id,
int.parse(matches[0]),
int.parse(matches[1]),
int.parse(matches[2]),
int.parse(matches[3]),
int.parse(matches[4]),
int.parse(matches[5]),
int.parse(matches[6])));
}
return cpus;
}
String _getCPUTemp(List<String> segments) {
const noMatch = "/sys/class/thermal/thermal_zone*/type";
final type = segments[0];
final value = segments[1];
// Not support to get CPU temperature
if (value.contains(noMatch) ||
type.contains(noMatch) ||
value.isEmpty ||
type.isEmpty) {
return '';
}
final split = type.split('\n');
int idx = 0;
for (var item in split) {
if (item.contains(cpuTempReg)) {
break;
}
idx++;
}
final valueSplited = value.split('\n');
if (idx >= valueSplited.length) return '';
final temp = int.tryParse(valueSplited[idx].trim());
if (temp == null) return '';
return '${(temp / 1000).toStringAsFixed(1)}°C';
}
List<NetSpeedPart> _parseNetSpeed(String raw) {
final split = raw.split('\n');
final deviceCount = split.length - 3;
if (deviceCount < 1) return [];
final time = int.parse(split[split.length - 1]);
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 results;
}

View File

@@ -38,7 +38,7 @@ class SnippetProvider extends BusyProvider {
void update(Snippet old, Snippet newOne) {
if (!have(old)) return;
_snippets[index(old)] = newOne;
locator<SnippetStore>().update(old, newOne);
locator<SnippetStore>().put(newOne);
notifyListeners();
}

View File

@@ -2,9 +2,9 @@
class BuildData {
static const String name = "ServerBox";
static const int build = 136;
static const int build = 159;
static const String engine =
"Flutter 3.0.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision ee4e09cce0 (9 days ago) • 2022-05-09 16:45:18 -0700\nEngine • revision d1b9a6938a\nTools • Dart 2.17.0 • DevTools 2.12.2\n";
static const String buildAt = "2022-05-19 15:59:10.728748";
static const int modifications = 15;
"Flutter 3.3.4 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision eb6d86ee27 (5 weeks ago) • 2022-10-04 22:31:45 -0700\nEngine • revision c08d7d5efc\nTools • Dart 2.18.2 • DevTools 2.15.0\n";
static const String buildAt = "2022-11-07 19:55:21.553007";
static const int modifications = 0;
}

View File

@@ -1,4 +1,26 @@
import 'package:toolbox/data/model/server/linux_icon.dart';
final linuxIcons = LinuxIcons([
'ubuntu',
'arch',
'centos',
'debian',
'fedora',
'opensuse',
'kali',
'wrt',
'armbian'
]);
final linuxIcons = LinuxIcons(
['ubuntu', 'arch', 'centos', 'debian', 'fedora', 'opensuse', 'kali']);
class LinuxIcons {
List<String> db;
LinuxIcons(this.db);
String? search(String sysVer) {
for (var item in db) {
if (sysVer.toLowerCase().contains(item)) {
return 'assets/linux/$item.png';
}
}
return null;
}
}

View File

@@ -3,4 +3,5 @@ const baseUrl = '$backendUrl/toolbox';
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
const myGithub = 'https://github.com/LollipopKit';
const rainSunMeGithub = 'https://github.com/RainSunMe';
const fectureGithub = 'https://github.com/fecture';
const issueUrl = 'https://github.com/LollipopKit/flutter_server_box/issues';

View File

@@ -3,11 +3,6 @@ import 'package:toolbox/data/model/app/update.dart';
import 'package:toolbox/data/res/url.dart';
class AppService {
Future<Map> getNotify() async {
final resp = await Dio().get('$baseUrl/notify.json');
return resp.data;
}
Future<AppUpdate> getUpdate() async {
final resp = await Dio().get('$baseUrl/update.json');
return AppUpdate.fromJson(resp.data);

View File

@@ -1,38 +1,28 @@
import 'dart:convert';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
class PrivateKeyStore extends PersistentStore {
void put(PrivateKeyInfo info) {
final ss = fetch();
if (!have(info)) ss.add(info);
box.put('key', json.encode(ss));
box.put(info.id, info);
}
List<PrivateKeyInfo> fetch() {
return getPrivateKeyInfoList(
json.decode(box.get('key', defaultValue: '[]')!));
final keys = box.keys;
final ps = <PrivateKeyInfo>[];
for (final key in keys) {
final s = box.get(key);
if (s != null) {
ps.add(s);
}
}
return ps;
}
PrivateKeyInfo get(String id) {
final ss = fetch();
return ss.firstWhere((e) => e.id == id);
return box.get(id);
}
void delete(PrivateKeyInfo s) {
final ss = fetch();
ss.removeAt(index(s));
box.put('key', json.encode(ss));
box.delete(s.id);
}
void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final ss = fetch();
ss[index(old)] = newInfo;
box.put('key', json.encode(ss));
}
int index(PrivateKeyInfo s) => fetch().indexWhere((e) => e.id == s.id);
bool have(PrivateKeyInfo s) => index(s) != -1;
}

View File

@@ -1,38 +1,33 @@
import 'dart:convert';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
class ServerStore extends PersistentStore {
void put(ServerPrivateInfo info) {
final ss = fetch();
if (!have(info)) ss.add(info);
box.put('servers', json.encode(ss));
box.put(info.id, info);
}
List<ServerPrivateInfo> fetch() {
return getServerInfoList(
json.decode(box.get('servers', defaultValue: '[]')!));
final ids = box.keys;
final List<ServerPrivateInfo> ss = [];
for (final id in ids) {
final s = box.get(id);
if (s != null) {
ss.add(s);
}
}
return ss;
}
void delete(ServerPrivateInfo s) {
final ss = fetch();
ss.removeAt(index(s));
box.put('servers', json.encode(ss));
box.delete(s.id);
}
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
final ss = fetch();
final idx = index(old);
if (idx < 0) {
throw RangeError.index(idx, ss);
if (!have(old)) {
throw Exception('Old ServerPrivateInfo not found');
}
ss[idx] = newInfo;
box.put('servers', json.encode(ss));
put(newInfo);
}
int index(ServerPrivateInfo s) => fetch()
.indexWhere((e) => e.ip == s.ip && e.port == s.port && e.user == e.user);
bool have(ServerPrivateInfo s) => index(s) != -1;
bool have(ServerPrivateInfo s) => box.get(s.id) != null;
}

View File

@@ -7,4 +7,7 @@ class SettingStore extends PersistentStore {
StoreProperty<int> get serverStatusUpdateInterval =>
property('serverStatusUpdateInterval', defaultValue: 2);
StoreProperty<int> get launchPage => property('launchPage', defaultValue: 0);
StoreProperty<int> get storeVersion =>
property('storeVersion', defaultValue: 0);
}

View File

@@ -1,32 +1,24 @@
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));
box.put(snippet.name, snippet);
}
List<Snippet> fetch() {
return getSnippetList(json.decode(box.get('snippet', defaultValue: '[]')!));
final keys = box.keys;
final ss = <Snippet>[];
for (final key in keys) {
final s = box.get(key);
if (s != null) {
ss.add(s);
}
}
return ss;
}
void delete(Snippet s) {
final ss = fetch();
ss.removeAt(index(s));
box.put('snippet', json.encode(ss));
box.delete(s.name);
}
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

@@ -20,28 +20,27 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
static String m0(rainSunMeGithub) =>
"\nThanks ${rainSunMeGithub} for participating in the test.\n\nAll rights reserved.";
static String m0(fileName) => "Download [${fileName}] to local?";
static String m1(fileName) => "Download [${fileName}] to local?";
static String m2(runningCount, stoppedCount) =>
static String m1(runningCount, stoppedCount) =>
"${runningCount} running, ${stoppedCount} container stopped.";
static String m3(count) => "${count} container running.";
static String m2(count) => "${count} container running.";
static String m4(percent, size) => "${percent}% of ${size}";
static String m3(percent, size) => "${percent}% of ${size}";
static String m5(count) => "Found ${count} update";
static String m4(count) => "Found ${count} update";
static String m6(code) => "request failed, status code: ${code}";
static String m5(code) => "request failed, status code: ${code}";
static String m7(url) =>
static String m6(url) =>
"Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don\'t have the above issues, please submit an issue on ${url}.";
static String m8(myGithub) => "\nMade with ❤️ by ${myGithub}";
static String m7(myGithub) => "\nMade with ❤️ by ${myGithub}";
static String m9(url) => "Please report bugs on ${url}";
static String m8(url) => "Please report bugs on ${url}";
static String m9(date) => "Are you sure to restore from ${date} ?";
static String m10(time) => "Spent time: ${time}";
@@ -57,7 +56,8 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": m0,
"aboutThanks": MessageLookupByLibrary.simpleMessage(
"\nAll rights reserved.\n\nThanks to the following people who participated in the test."),
"addAServer": MessageLookupByLibrary.simpleMessage("add a server"),
"addPrivateKey":
MessageLookupByLibrary.simpleMessage("Add private key"),
@@ -67,6 +67,11 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("App primary color"),
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
"backDir": MessageLookupByLibrary.simpleMessage("Back"),
"backup": MessageLookupByLibrary.simpleMessage("Backup"),
"backupTip": MessageLookupByLibrary.simpleMessage(
"The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting)."),
"backupVersionNotMatch": MessageLookupByLibrary.simpleMessage(
"Backup version is not match."),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
"chooseDestination":
@@ -74,6 +79,7 @@ class MessageLookup extends MessageLookupByLibrary {
"choosePrivateKey":
MessageLookupByLibrary.simpleMessage("Choose private key"),
"clear": MessageLookupByLibrary.simpleMessage("Clear"),
"clickSee": MessageLookupByLibrary.simpleMessage("Click here"),
"close": MessageLookupByLibrary.simpleMessage("Close"),
"containerStatus":
MessageLookupByLibrary.simpleMessage("Container status"),
@@ -87,13 +93,13 @@ class MessageLookup extends MessageLookupByLibrary {
"decode": MessageLookupByLibrary.simpleMessage("Decode"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
"disconnected": MessageLookupByLibrary.simpleMessage("Disconnected"),
"dl2Local": m1,
"dockerStatusRunningAndStoppedFmt": m2,
"dockerStatusRunningFmt": m3,
"dl2Local": m0,
"dockerStatusRunningAndStoppedFmt": m1,
"dockerStatusRunningFmt": m2,
"download": MessageLookupByLibrary.simpleMessage("Download"),
"downloadFinished":
MessageLookupByLibrary.simpleMessage("Download finished"),
"downloadStatus": m4,
"downloadStatus": m3,
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"encode": MessageLookupByLibrary.simpleMessage("Encode"),
"error": MessageLookupByLibrary.simpleMessage("Error"),
@@ -101,22 +107,28 @@ class MessageLookup extends MessageLookupByLibrary {
"experimentalFeature":
MessageLookupByLibrary.simpleMessage("Experimental feature"),
"export": MessageLookupByLibrary.simpleMessage("Export"),
"feedback": MessageLookupByLibrary.simpleMessage("Feedback"),
"feedbackOnGithub": MessageLookupByLibrary.simpleMessage(
"If you have any questions, please feedback on Github."),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage(
"These fields must not be empty."),
"files": MessageLookupByLibrary.simpleMessage("Files"),
"foundNUpdate": m5,
"foundNUpdate": m4,
"go": MessageLookupByLibrary.simpleMessage("Go"),
"goSftpDlPage":
MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"),
"host": MessageLookupByLibrary.simpleMessage("Host"),
"httpFailedWithCode": m6,
"httpFailedWithCode": m5,
"import": MessageLookupByLibrary.simpleMessage("Import"),
"importAndExport":
MessageLookupByLibrary.simpleMessage("Import and Export"),
"inputDomainHere":
MessageLookupByLibrary.simpleMessage("Input Domain here"),
"install": MessageLookupByLibrary.simpleMessage("install"),
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
"Please https://docs.docker.com/engine/install docker first."),
"invalidVersionHelp": m7,
"invalidJson": MessageLookupByLibrary.simpleMessage("Invalid JSON"),
"invalidVersionHelp": m6,
"keepForeground":
MessageLookupByLibrary.simpleMessage("Keep app foreground!"),
"keyAuth": MessageLookupByLibrary.simpleMessage("Key Auth"),
@@ -125,8 +137,8 @@ class MessageLookup extends MessageLookupByLibrary {
"license": MessageLookupByLibrary.simpleMessage("License"),
"loadingFiles":
MessageLookupByLibrary.simpleMessage("Loading files..."),
"loss": MessageLookupByLibrary.simpleMessage("Loss"),
"madeWithLove": m8,
"loss": MessageLookupByLibrary.simpleMessage("loss"),
"madeWithLove": m7,
"max": MessageLookupByLibrary.simpleMessage("max"),
"min": MessageLookupByLibrary.simpleMessage("min"),
"ms": MessageLookupByLibrary.simpleMessage("ms"),
@@ -148,19 +160,23 @@ class MessageLookup extends MessageLookupByLibrary {
"pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage(
"Please input a target IP/domain."),
"pingNoServer": MessageLookupByLibrary.simpleMessage(
"No server to ping.\nPlease add a server in server tab."),
"platformNotSupportUpdate": MessageLookupByLibrary.simpleMessage(
"Current platform does not support in app update.\nPlease build from source and install it."),
"plzEnterHost":
MessageLookupByLibrary.simpleMessage("Please enter host."),
"plzEnterPwd":
MessageLookupByLibrary.simpleMessage("Please enter password."),
"plzSelectKey":
MessageLookupByLibrary.simpleMessage("Please select a key."),
"port": MessageLookupByLibrary.simpleMessage("Port"),
"privateKey": MessageLookupByLibrary.simpleMessage("Private Key"),
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
"reportBugsOnGithubIssue": m9,
"reportBugsOnGithubIssue": m8,
"restore": MessageLookupByLibrary.simpleMessage("Restore"),
"restoreSuccess": MessageLookupByLibrary.simpleMessage(
"Restore success. Restart app to apply."),
"restoreSureWithDate": m9,
"result": MessageLookupByLibrary.simpleMessage("Result"),
"run": MessageLookupByLibrary.simpleMessage("Run"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
@@ -188,12 +204,15 @@ class MessageLookup extends MessageLookupByLibrary {
"start": MessageLookupByLibrary.simpleMessage("Start"),
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
"sureDelete": m11,
"sureNoPwd": MessageLookupByLibrary.simpleMessage(
"Are you sure to use no password?"),
"sureToDeleteServer": m12,
"ttl": MessageLookupByLibrary.simpleMessage("TTL"),
"ttl": MessageLookupByLibrary.simpleMessage("ttl"),
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
"unkownConvertMode":
MessageLookupByLibrary.simpleMessage("Unknown convert mode"),
"update": MessageLookupByLibrary.simpleMessage("Update"),
"updateAll": MessageLookupByLibrary.simpleMessage("Update all"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"You set to 0, will not update automatically.\nYou can pull to refresh manually."),

View File

@@ -20,28 +20,27 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'zh';
static String m0(rainSunMeGithub) =>
"\n感谢 ${rainSunMeGithub} 参与软件测试。\n\n保留所有权利。";
static String m0(fileName) => "下载 [${fileName}] 到本地?";
static String m1(fileName) => "下载 [${fileName}] 到本地?";
static String m2(runningCount, stoppedCount) =>
static String m1(runningCount, stoppedCount) =>
"${runningCount}个正在运行, ${stoppedCount}个已停止";
static String m3(count) => "${count}个容器正在运行";
static String m2(count) => "${count}个容器正在运行";
static String m4(percent, size) => "${size}${percent}%";
static String m3(percent, size) => "${size}${percent}%";
static String m5(count) => "找到 ${count} 个更新";
static String m4(count) => "找到 ${count} 个更新";
static String m6(code) => "请求失败, 状态码: ${code}";
static String m5(code) => "请求失败, 状态码: ${code}";
static String m7(url) =>
static String m6(url) =>
"请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 ${url} 提交问题。";
static String m8(myGithub) => "\n用❤️制作 by ${myGithub}";
static String m7(myGithub) => "\n用❤️制作 by ${myGithub}";
static String m9(url) => "请到 ${url} 提交问题";
static String m8(url) => "请到 ${url} 提交问题";
static String m9(date) => "确定恢复 ${date} 的备份吗?";
static String m10(time) => "耗时: ${time}";
@@ -57,18 +56,25 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": m0,
"aboutThanks":
MessageLookupByLibrary.simpleMessage("\n保留所有权利。\n\n感谢以下参与软件测试的各位。"),
"addAServer": MessageLookupByLibrary.simpleMessage("添加服务器"),
"addPrivateKey": MessageLookupByLibrary.simpleMessage("添加一个私钥"),
"alreadyLastDir": MessageLookupByLibrary.simpleMessage("已经是最上层目录了"),
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
"attention": MessageLookupByLibrary.simpleMessage("注意"),
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
"backup": MessageLookupByLibrary.simpleMessage("备份"),
"backupTip": MessageLookupByLibrary.simpleMessage(
"导出的数据仅进行了简单加密,请妥善保管。\n除了设置项,恢复的数据不会覆盖现有数据。"),
"backupVersionNotMatch":
MessageLookupByLibrary.simpleMessage("备份版本不匹配,无法恢复"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
"choose": MessageLookupByLibrary.simpleMessage("选择"),
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
"choosePrivateKey": MessageLookupByLibrary.simpleMessage("选择私钥"),
"clear": MessageLookupByLibrary.simpleMessage("清除"),
"clickSee": MessageLookupByLibrary.simpleMessage("点击查看"),
"close": MessageLookupByLibrary.simpleMessage("关闭"),
"containerStatus": MessageLookupByLibrary.simpleMessage("容器状态"),
"convert": MessageLookupByLibrary.simpleMessage("转换"),
@@ -81,31 +87,36 @@ class MessageLookup extends MessageLookupByLibrary {
"decode": MessageLookupByLibrary.simpleMessage("解码"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"disconnected": MessageLookupByLibrary.simpleMessage("连接断开"),
"dl2Local": m1,
"dockerStatusRunningAndStoppedFmt": m2,
"dockerStatusRunningFmt": m3,
"dl2Local": m0,
"dockerStatusRunningAndStoppedFmt": m1,
"dockerStatusRunningFmt": m2,
"download": MessageLookupByLibrary.simpleMessage("下载"),
"downloadFinished": MessageLookupByLibrary.simpleMessage("下载完成!"),
"downloadStatus": m4,
"downloadStatus": m3,
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"encode": MessageLookupByLibrary.simpleMessage("编码"),
"error": MessageLookupByLibrary.simpleMessage("出错了"),
"exampleName": MessageLookupByLibrary.simpleMessage("名称示例"),
"experimentalFeature": MessageLookupByLibrary.simpleMessage("实验性功能"),
"export": MessageLookupByLibrary.simpleMessage("导出"),
"feedback": MessageLookupByLibrary.simpleMessage("反馈"),
"feedbackOnGithub":
MessageLookupByLibrary.simpleMessage("如果你有任何问题请在GitHub反馈"),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage("这些输入框不能为空。"),
"files": MessageLookupByLibrary.simpleMessage("文件"),
"foundNUpdate": m5,
"foundNUpdate": m4,
"go": MessageLookupByLibrary.simpleMessage("开始"),
"goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"),
"host": MessageLookupByLibrary.simpleMessage("主机"),
"httpFailedWithCode": m6,
"httpFailedWithCode": m5,
"import": MessageLookupByLibrary.simpleMessage("导入"),
"importAndExport": MessageLookupByLibrary.simpleMessage("导入或导出"),
"inputDomainHere": MessageLookupByLibrary.simpleMessage("在这里输入域名"),
"install": MessageLookupByLibrary.simpleMessage("安装"),
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
"请先 https://docs.docker.com/engine/install docker"),
"invalidVersionHelp": m7,
"invalidJson": MessageLookupByLibrary.simpleMessage("无效的json存在格式问题"),
"invalidVersionHelp": m6,
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
"lastTry": MessageLookupByLibrary.simpleMessage("最后尝试"),
@@ -113,7 +124,7 @@ class MessageLookup extends MessageLookupByLibrary {
"license": MessageLookupByLibrary.simpleMessage("开源证书"),
"loadingFiles": MessageLookupByLibrary.simpleMessage("正在加载目录。。。"),
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
"madeWithLove": m8,
"madeWithLove": m7,
"max": MessageLookupByLibrary.simpleMessage("最大"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"ms": MessageLookupByLibrary.simpleMessage("毫秒"),
@@ -130,16 +141,21 @@ class MessageLookup extends MessageLookupByLibrary {
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("平均:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"),
"pingNoServer": MessageLookupByLibrary.simpleMessage(
"没有服务器可用于Ping\n请在服务器tab添加服务器后再试"),
"platformNotSupportUpdate":
MessageLookupByLibrary.simpleMessage("当前平台不支持更新,请编译最新源码后手动安装"),
"plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"),
"plzEnterPwd": MessageLookupByLibrary.simpleMessage("请输入密码"),
"plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"),
"port": MessageLookupByLibrary.simpleMessage("端口"),
"privateKey": MessageLookupByLibrary.simpleMessage("私钥"),
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
"reportBugsOnGithubIssue": m9,
"reportBugsOnGithubIssue": m8,
"restore": MessageLookupByLibrary.simpleMessage("恢复"),
"restoreSuccess":
MessageLookupByLibrary.simpleMessage("恢复成功需要重启App来应用更改"),
"restoreSureWithDate": m9,
"result": MessageLookupByLibrary.simpleMessage("结果"),
"run": MessageLookupByLibrary.simpleMessage("运行"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
@@ -162,11 +178,13 @@ class MessageLookup extends MessageLookupByLibrary {
"start": MessageLookupByLibrary.simpleMessage("开始"),
"stop": MessageLookupByLibrary.simpleMessage("停止"),
"sureDelete": m11,
"sureNoPwd": MessageLookupByLibrary.simpleMessage("确认使用无密码?"),
"sureToDeleteServer": m12,
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
"unkownConvertMode": MessageLookupByLibrary.simpleMessage("未知转换模式"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"updateAll": MessageLookupByLibrary.simpleMessage("更新全部"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"你设置为0服务器状态不会自动刷新。\n你可以手动下拉刷新。"),

View File

@@ -150,13 +150,13 @@ class S {
);
}
/// `\nThanks {rainSunMeGithub} for participating in the test.\n\nAll rights reserved.`
String aboutThanks(Object rainSunMeGithub) {
/// `\nAll rights reserved.\n\nThanks to the following people who participated in the test.`
String get aboutThanks {
return Intl.message(
'\nThanks $rainSunMeGithub for participating in the test.\n\nAll rights reserved.',
'\nAll rights reserved.\n\nThanks to the following people who participated in the test.',
name: 'aboutThanks',
desc: '',
args: [rainSunMeGithub],
args: [],
);
}
@@ -330,20 +330,20 @@ class S {
);
}
/// `TTL`
/// `ttl`
String get ttl {
return Intl.message(
'TTL',
'ttl',
name: 'ttl',
desc: '',
args: [],
);
}
/// `Loss`
/// `loss`
String get loss {
return Intl.message(
'Loss',
'loss',
name: 'loss',
desc: '',
args: [],
@@ -790,11 +790,11 @@ class S {
);
}
/// `Please enter password.`
String get plzEnterPwd {
/// `Are you sure to use no password?`
String get sureNoPwd {
return Intl.message(
'Please enter password.',
name: 'plzEnterPwd',
'Are you sure to use no password?',
name: 'sureNoPwd',
desc: '',
args: [],
);
@@ -1220,6 +1220,136 @@ class S {
args: [],
);
}
/// `No server to ping.\nPlease add a server in server tab.`
String get pingNoServer {
return Intl.message(
'No server to ping.\nPlease add a server in server tab.',
name: 'pingNoServer',
desc: '',
args: [],
);
}
/// `The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).`
String get backupTip {
return Intl.message(
'The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).',
name: 'backupTip',
desc: '',
args: [],
);
}
/// `Backup`
String get backup {
return Intl.message(
'Backup',
name: 'backup',
desc: '',
args: [],
);
}
/// `Restore`
String get restore {
return Intl.message(
'Restore',
name: 'restore',
desc: '',
args: [],
);
}
/// `Are you sure to restore from {date} ?`
String restoreSureWithDate(Object date) {
return Intl.message(
'Are you sure to restore from $date ?',
name: 'restoreSureWithDate',
desc: '',
args: [date],
);
}
/// `Backup version is not match.`
String get backupVersionNotMatch {
return Intl.message(
'Backup version is not match.',
name: 'backupVersionNotMatch',
desc: '',
args: [],
);
}
/// `Invalid JSON`
String get invalidJson {
return Intl.message(
'Invalid JSON',
name: 'invalidJson',
desc: '',
args: [],
);
}
/// `Restore success. Restart app to apply.`
String get restoreSuccess {
return Intl.message(
'Restore success. Restart app to apply.',
name: 'restoreSuccess',
desc: '',
args: [],
);
}
/// `Click here`
String get clickSee {
return Intl.message(
'Click here',
name: 'clickSee',
desc: '',
args: [],
);
}
/// `Feedback`
String get feedback {
return Intl.message(
'Feedback',
name: 'feedback',
desc: '',
args: [],
);
}
/// `If you have any questions, please feedback on Github.`
String get feedbackOnGithub {
return Intl.message(
'If you have any questions, please feedback on Github.',
name: 'feedbackOnGithub',
desc: '',
args: [],
);
}
/// `Update`
String get update {
return Intl.message(
'Update',
name: 'update',
desc: '',
args: [],
);
}
/// `Input Domain here`
String get inputDomainHere {
return Intl.message(
'Input Domain here',
name: 'inputDomainHere',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -9,7 +9,7 @@
"snippet": "Snippet",
"privateKey": "Private Key",
"madeWithLove": "\nMade with ❤️ by {myGithub}",
"aboutThanks": "\nThanks {rainSunMeGithub} for participating in the test.\n\nAll rights reserved.",
"aboutThanks": "\nAll rights reserved.\n\nThanks to the following people who participated in the test.",
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
"serverTabLoading": "Loading...",
"serverTabPlzSave": "Please 'save' this private key again.",
@@ -27,8 +27,8 @@
"min": "min",
"max": "max",
"ms": "ms",
"ttl": "TTL",
"loss": "Loss",
"ttl": "ttl",
"loss": "loss",
"noResult": "No result",
"pingInputIP": "Please input a target IP/domain.",
"clear": "Clear",
@@ -73,7 +73,7 @@
"addPrivateKey": "Add private key",
"choosePrivateKey": "Choose private key",
"plzEnterHost": "Please enter host.",
"plzEnterPwd": "Please enter password.",
"sureNoPwd": "Are you sure to use no password?",
"plzSelectKey": "Please select a key.",
"exampleName": "Example name",
"stop": "Stop",
@@ -115,5 +115,18 @@
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
"invalidVersionHelp": "Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don't have the above issues, please submit an issue on {url}.",
"noInterface": "No interface",
"lastTry": "Last try!"
"lastTry": "Last try!",
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).",
"backup": "Backup",
"restore": "Restore",
"restoreSureWithDate": "Are you sure to restore from {date} ?",
"backupVersionNotMatch": "Backup version is not match.",
"invalidJson": "Invalid JSON",
"restoreSuccess": "Restore success. Restart app to apply.",
"clickSee": "Click here",
"feedback": "Feedback",
"feedbackOnGithub": "If you have any questions, please feedback on Github.",
"update": "Update",
"inputDomainHere": "Input Domain here"
}

View File

@@ -9,7 +9,7 @@
"snippet": "代码片段",
"privateKey": "私钥",
"madeWithLove": "\n用❤制作 by {myGithub}",
"aboutThanks": "\n感谢 {rainSunMeGithub} 参与软件测试。\n\n保留所有权利。",
"aboutThanks": "\n保留所有权利。\n\n感谢以下参与软件测试的各位。",
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
"serverTabLoading": "加载中...",
"serverTabPlzSave": "请再次保存该私钥",
@@ -73,7 +73,7 @@
"addPrivateKey": "添加一个私钥",
"choosePrivateKey": "选择私钥",
"plzEnterHost": "请输入主机",
"plzEnterPwd": "请输入密码",
"sureNoPwd": "确认使用无密码",
"plzSelectKey": "请选择私钥",
"exampleName": "名称示例",
"stop": "停止",
@@ -115,5 +115,18 @@
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
"invalidVersionHelp": "请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 {url} 提交问题。",
"noInterface": "没有可用的接口",
"lastTry": "最后尝试"
"lastTry": "最后尝试",
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。\n除了设置项恢复的数据不会覆盖现有数据。",
"backup": "备份",
"restore": "恢复",
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
"invalidJson": "无效的json存在格式问题",
"restoreSuccess": "恢复成功需要重启App来应用更改",
"clickSee": "点击查看",
"feedback": "反馈",
"feedbackOnGithub": "如果你有任何问题请在GitHub反馈",
"update": "更新",
"inputDomainHere": "在这里输入域名"
}

View File

@@ -6,6 +6,10 @@ import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/app.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/persistant_store.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/snippet.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/apt.dart';
import 'package:toolbox/data/provider/debug.dart';
@@ -14,11 +18,17 @@ import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/data/store/snippet.dart';
import 'package:toolbox/locator.dart';
Future<void> initApp() async {
await Hive.initFlutter();
await initHive();
await setupLocator();
await upgradeStore();
locator<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData();
@@ -30,6 +40,27 @@ Future<void> initApp() async {
});
}
Future<void> initHive() async {
await Hive.initFlutter();
Hive.registerAdapter(SnippetAdapter());
Hive.registerAdapter(PrivateKeyInfoAdapter());
Hive.registerAdapter(ServerPrivateInfoAdapter());
}
Future<void> upgradeStore() async {
final setting = locator<SettingStore>();
final version = setting.storeVersion.fetch();
if (version == 0) {
final snippet = locator<SnippetStore>();
final key = locator<PrivateKeyStore>();
final spi = locator<ServerStore>();
for (final s in <PersistentStore>[snippet, key, spi]) {
await s.box.deleteAll(s.box.keys);
}
setting.storeVersion.put(1);
}
}
void runInZone(dynamic Function() body) {
final zoneSpec = ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {

View File

@@ -29,6 +29,7 @@ class _AptManagePageState extends State<AptManagePage>
final greyStyle = const TextStyle(color: Colors.grey);
final scrollController = ScrollController();
final scrollControllerUpdate = ScrollController();
final textController = TextEditingController();
final _aptProvider = locator<AptProvider>();
late S s;
@@ -37,7 +38,6 @@ class _AptManagePageState extends State<AptManagePage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
_aptProvider.refreshInstalled();
}
@override
@@ -59,38 +59,41 @@ class _AptManagePageState extends State<AptManagePage>
}
// ignore: prefer_function_declarations_over_variables
PwdRequestFunc onPwdRequest = (lastTime, user) async {
Function onSubmitted = () {
if (textController.text == '') {
showRoundDialog(context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok)),
]);
return;
}
Navigator.of(context).pop();
};
// ignore: prefer_function_declarations_over_variables
PwdRequestFunc onPwdRequest = (triedTimes, user) async {
if (!mounted) return '';
final textController = TextEditingController();
await showRoundDialog(
context,
lastTime ? s.lastTry : (user ?? s.unknown),
triedTimes == 3 ? s.lastTry : (user ?? s.unknown),
TextField(
controller: textController,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
onSubmitted: (_) => textController.text.trim(),
onSubmitted: (_) => onSubmitted(),
decoration: InputDecoration(
labelText: s.pwd,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Text(s.cancel)),
TextButton(
onPressed: () => onSubmitted(),
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
@@ -105,8 +108,9 @@ class _AptManagePageState extends State<AptManagePage>
() =>
scrollController.jumpTo(scrollController.position.maxScrollExtent),
() => scrollControllerUpdate
.jumpTo(scrollControllerUpdate.position.maxScrollExtent),
.jumpTo(scrollControllerUpdate.positions.last.maxScrollExtent),
onPwdRequest);
_aptProvider.refreshInstalled();
}
@override
@@ -129,9 +133,19 @@ class _AptManagePageState extends State<AptManagePage>
const SizedBox(
height: 37,
),
Text(
apt.error!,
textAlign: TextAlign.center,
SizedBox(
height: _media.size.height * 0.4,
child: Padding(
padding: const EdgeInsets.all(17),
child: RoundRectCard(
SingleChildScrollView(
padding: const EdgeInsets.all(17),
child: Text(
apt.error!,
textAlign: TextAlign.center,
)),
),
),
),
],
);
@@ -141,7 +155,9 @@ class _AptManagePageState extends State<AptManagePage>
}
if (apt.updateLog != null && apt.upgradeable == null) {
return SizedBox(
height: _media.size.height * 0.7,
height: _media.size.height -
_media.padding.top -
_media.padding.bottom,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: SingleChildScrollView(

215
lib/view/page/backup.dart Normal file
View File

@@ -0,0 +1,215 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toolbox/core/extension/colorx.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/backup.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/data/store/snippet.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
const backupFormatVersion = 1;
class BackupPage extends StatelessWidget {
BackupPage({Key? key}) : super(key: key);
final setting = locator<SettingStore>();
final server = locator<ServerStore>();
final snippet = locator<SnippetStore>();
final privateKey = locator<PrivateKeyStore>();
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context);
final s = S.of(context);
return Scaffold(
appBar: AppBar(
title: Text(s.importAndExport, style: size18),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(37),
child: Text(
s.backupTip,
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 107,
),
_buildCard(s.restore, Icons.download, media,
() => _showImportDialog(context, s)),
const SizedBox(height: 7),
const Divider(),
const SizedBox(height: 7),
_buildCard(s.backup, Icons.file_upload, media,
() => _showExportDialog(context, s))
],
)),
);
}
Widget _buildCard(String text, IconData icon, MediaQueryData media,
FutureOr Function() onTap) {
final priColor = primaryColor;
final textColor = priColor.isBrightColor ? Colors.black : Colors.white;
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(37), color: priColor),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: textColor,
),
const SizedBox(width: 7),
Text(text, style: TextStyle(color: textColor)),
],
),
),
),
);
}
Future<void> _showExportDialog(BuildContext context, S s) async {
final exportFieldController = TextEditingController()
..text = _diyEncrtpt(json.encode(Backup(
backupFormatVersion,
DateTime.now().toString().split('.').first,
server.fetch(),
snippet.fetch(),
privateKey.fetch(),
setting.primaryColor.fetch() ?? Colors.pinkAccent.value,
setting.serverStatusUpdateInterval.fetch() ?? 2,
setting.launchPage.fetch() ?? 0,
)));
await showRoundDialog(
context,
s.export,
TextField(
decoration: const InputDecoration(
labelText: 'JSON',
),
maxLines: 7,
controller: exportFieldController,
),
[
TextButton(
child: Text(s.copy),
onPressed: () {
Clipboard.setData(
ClipboardData(text: exportFieldController.text));
Navigator.pop(context);
},
),
]);
}
Future<void> _showImportDialog(BuildContext context, S s) async {
final importFieldController = TextEditingController();
await showRoundDialog(
context,
s.import,
TextField(
decoration: const InputDecoration(
labelText: 'JSON',
),
maxLines: 3,
controller: importFieldController,
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async =>
await _import(importFieldController.text.trim(), context, s),
child: const Text('GO'),
)
]);
}
Future<void> _import(String text, BuildContext context, S s) async {
if (text.isEmpty) {
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
_importBackup(text, context, s);
Navigator.of(context).pop();
}
Future<void> _importBackup(String raw, BuildContext context, S s) async {
try {
final backup = await compute(_decode, raw);
if (backupFormatVersion != backup.version) {
showSnackBar(context, Text(s.backupVersionNotMatch));
return;
}
await showRoundDialog(
context, s.attention, Text(s.restoreSureWithDate(backup.date)), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel),
),
TextButton(
onPressed: () async {
for (final s in backup.snippets) {
snippet.put(s);
}
for (final s in backup.spis) {
server.put(s);
}
for (final s in backup.keys) {
privateKey.put(s);
}
setting.primaryColor.put(backup.primaryColor);
setting.serverStatusUpdateInterval
.put(backup.serverStatusUpdateInterval);
setting.launchPage.put(backup.launchPage);
Navigator.of(context).pop();
showSnackBar(context, Text(s.restoreSuccess));
},
child: Text(s.ok),
),
]);
} catch (e) {
showSnackBar(context, Text(s.invalidJson));
return;
}
}
}
Backup _decode(String raw) {
final decrypted = _diyDecrypt(raw);
return Backup.fromJson(json.decode(decrypted));
}
String _diyEncrtpt(String raw) =>
json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
String _diyDecrypt(String raw) {
final list = json.decode(raw);
final sb = StringBuffer();
for (final e in list) {
sb.writeCharCode((e - 1) ~/ 2);
}
return sb.toString();
}

View File

@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/generated/l10n.dart';
@@ -44,22 +44,20 @@ class _ConvertPageState extends State<ConvertPage>
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: GestureDetector(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7),
controller: ScrollController(),
child: Column(children: [
const SizedBox(height: 13),
_buildInputTop(),
_buildTypeOption(),
_buildResult(),
])),
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7),
controller: ScrollController(),
child: Column(children: [
const SizedBox(height: 13),
_buildInputTop(),
_buildTypeOption(),
_buildResult(),
])),
floatingActionButton: FloatingActionButton(
onPressed: () {
try {
_textEditingControllerResult.text = doConvert();
FocusScope.of(context).requestFocus(FocusNode());
} catch (e) {
showSnackBar(context, Text('Error: \n$e'));
}
@@ -89,8 +87,7 @@ class _ConvertPageState extends State<ConvertPage>
Widget _buildInputTop() {
return SizedBox(
height: _media.size.height * 0.33,
child: buildInput(context, _textEditingController,
onSubmitted: (_) => _textEditingControllerResult.text = doConvert()),
child: buildInput(context, _textEditingController),
);
}
@@ -123,10 +120,12 @@ class _ConvertPageState extends State<ConvertPage>
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Icon(Icons.copy, semanticLabel: s.copy),
onPressed: () => FlutterClipboard.copy(
_textEditingControllerResult.text == ''
? ' '
: _textEditingControllerResult.text),
onPressed: () => Clipboard.setData(
ClipboardData(
text: _textEditingControllerResult.text == ''
? ' '
: _textEditingControllerResult.text),
),
)
],
),
@@ -157,8 +156,8 @@ class _ConvertPageState extends State<ConvertPage>
title: Text(
e,
style: TextStyle(
color:
_theme.textTheme.bodyText2!.color!.withAlpha(177)),
color: _theme.textTheme.bodyText2!.color!.withAlpha(177),
),
),
trailing: _buildRadio(typeOption.indexOf(e)),
))

View File

@@ -1,4 +1,3 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/utils.dart';
@@ -52,7 +51,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
Navigator.of(context).pop();
return;
}
_docker.init(client);
_docker.init(client, widget.spi.user);
}
@override
@@ -152,49 +151,31 @@ class _DockerManagePageState extends State<DockerManagePage> {
Widget _buildMoreBtn(bool running, String containerId) {
final item = running ? DockerMenuItems.stop : DockerMenuItems.start;
return DropdownButtonHideUnderline(
child: DropdownButton2(
customButton: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(
Icons.more_vert,
size: 17,
),
return buildPopuopMenu(
items: [
PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
customItemsHeight: 8,
items: [
DropdownMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
DropdownMenuItem<DropdownBtnItem>(
value: DockerMenuItems.rm,
child: DockerMenuItems.rm.build,
),
],
onChanged: (value) {
final item = value as DropdownBtnItem;
switch (item) {
case DockerMenuItems.rm:
_docker.delete(containerId);
break;
case DockerMenuItems.start:
_docker.start(containerId);
break;
case DockerMenuItems.stop:
_docker.stop(containerId);
break;
}
},
itemHeight: 37,
itemPadding: const EdgeInsets.only(left: 17, right: 17),
dropdownWidth: 133,
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
PopupMenuItem<DropdownBtnItem>(
value: DockerMenuItems.rm,
child: DockerMenuItems.rm.build,
),
dropdownElevation: 8,
offset: const Offset(0, 8),
),
],
onSelected: (value) {
final item = value as DropdownBtnItem;
switch (item) {
case DockerMenuItems.rm:
_docker.delete(containerId);
break;
case DockerMenuItems.start:
_docker.start(containerId);
break;
case DockerMenuItems.stop:
_docker.stop(containerId);
break;
}
},
);
}

View File

@@ -1,6 +1,6 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter_advanced_drawer/flutter_advanced_drawer.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/build_mode.dart';
@@ -18,6 +18,7 @@ import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/backup.dart';
import 'package:toolbox/view/page/convert.dart';
import 'package:toolbox/view/page/debug.dart';
import 'package:toolbox/view/page/ping.dart';
@@ -44,7 +45,6 @@ class _MyHomePageState extends State<MyHomePage>
WidgetsBindingObserver {
late final ServerProvider _serverProvider;
late final PageController _pageController;
late final AdvancedDrawerController _advancedDrawerController;
late int _selectIndex;
late double _width;
late S s;
@@ -56,7 +56,6 @@ class _MyHomePageState extends State<MyHomePage>
WidgetsBinding.instance.addObserver(this);
_selectIndex = locator<SettingStore>().launchPage.fetch()!;
_pageController = PageController(initialPage: _selectIndex);
_advancedDrawerController = AdvancedDrawerController();
}
@override
@@ -90,8 +89,9 @@ class _MyHomePageState extends State<MyHomePage>
return WillPopScope(
child: _buildMain(context),
onWillPop: () {
if (_advancedDrawerController.value.visible) {
_advancedDrawerController.hideDrawer();
final scaffold = Scaffold.of(context);
if (scaffold.isDrawerOpen) {
scaffold.closeDrawer();
return Future.value(false);
}
return Future.value(true);
@@ -99,63 +99,31 @@ class _MyHomePageState extends State<MyHomePage>
}
Widget _buildMain(BuildContext context) {
return AdvancedDrawer(
controller: _advancedDrawerController,
animationCurve: Curves.easeInOut,
animationDuration: const Duration(milliseconds: 300),
animateChildDecoration: true,
rtlOpening: false,
childDecoration: const BoxDecoration(
// NOTICE: Uncomment if you want to add shadow behind the page.
// Keep in mind that it may cause animation jerks.
// boxShadow: <BoxShadow>[
// BoxShadow(
// color: Colors.black12,
// blurRadius: 0.0,
// ),
// ],
borderRadius: BorderRadius.all(Radius.circular(16)),
),
drawer: _buildDrawer(),
child: Scaffold(
appBar: AppBar(
title: Text(tabTitleName(context, _selectIndex), style: size18),
actions: [
IconButton(
icon: const Icon(Icons.developer_mode, size: 23),
tooltip: s.debug,
onPressed: () =>
AppRoute(const DebugPage(), 'Debug Page').go(context),
),
],
leading: IconButton(
onPressed: () => _advancedDrawerController.showDrawer(),
icon: ValueListenableBuilder<AdvancedDrawerValue>(
valueListenable: _advancedDrawerController,
builder: (_, value, __) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: Icon(
value.visible ? Icons.clear : Icons.menu,
key: ValueKey<bool>(value.visible),
),
);
},
),
),
return Scaffold(
drawer: _buildDrawer(),
appBar: AppBar(
title: Text(tabTitleName(context, _selectIndex), style: size18),
actions: [
IconButton(
icon: const Icon(Icons.developer_mode, size: 23),
tooltip: s.debug,
onPressed: () =>
AppRoute(const DebugPage(), 'Debug Page').go(context),
),
body: PageView(
physics: const ClampingScrollPhysics(),
controller: _pageController,
onPageChanged: (i) {
FocusScope.of(context).unfocus();
_selectIndex = i;
setState(() {});
},
children: const [ServerPage(), ConvertPage(), PingPage()],
),
bottomNavigationBar: _buildBottom(context),
));
],
),
body: PageView(
physics: const ClampingScrollPhysics(),
controller: _pageController,
onPageChanged: (i) {
FocusScope.of(context).unfocus();
_selectIndex = i;
setState(() {});
},
children: const [ServerPage(), ConvertPage(), PingPage()],
),
bottomNavigationBar: _buildBottom(context),
);
}
Widget _buildItem(int idx, NavigationItem item, bool isSelected) {
@@ -206,7 +174,7 @@ class _MyHomePageState extends State<MyHomePage>
}
Widget _buildDrawer() {
return SafeArea(
return Drawer(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -240,6 +208,29 @@ class _MyHomePageState extends State<MyHomePage>
AppRoute(const SFTPDownloadedPage(), 'snippet list')
.go(context),
),
ListTile(
leading: const Icon(Icons.import_export),
title: Text(s.backup),
onTap: () =>
AppRoute(BackupPage(), 'backup page').go(context),
),
ListTile(
leading: const Icon(Icons.info),
title: Text(s.feedback),
onTap: () => showRoundDialog(
context, s.feedback, Text(s.feedbackOnGithub), [
TextButton(
onPressed: () => Clipboard.setData(
const ClipboardData(text: issueUrl)),
child: Text(s.copy)),
TextButton(
onPressed: () => openUrl(issueUrl),
child: Text(s.feedback)),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.close))
]),
),
ListTile(
leading: const Icon(Icons.snippet_folder),
title: Text(s.snippet),
@@ -255,9 +246,16 @@ class _MyHomePageState extends State<MyHomePage>
UrlText(
text: s.madeWithLove(myGithub), replace: 'LollipopKit'),
UrlText(
text: s.aboutThanks(rainSunMeGithub),
text: s.aboutThanks,
),
const UrlText(
text: rainSunMeGithub,
replace: 'RainSunMe',
),
const UrlText(
text: fectureGithub,
replace: 'fecture',
)
],
child: Text(s.license),
)

View File

@@ -9,6 +9,12 @@ import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
final doaminReg =
RegExp(r'^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$');
final ipv4Reg =
RegExp(r'^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$');
final ipv6Reg = RegExp(r'^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$');
class PingPage extends StatefulWidget {
const PingPage({Key? key}) : super(key: key);
@@ -21,6 +27,7 @@ class _PingPageState extends State<PingPage>
late TextEditingController _textEditingController;
late MediaQueryData _media;
final List<PingResult> _results = [];
final _serverProvider = locator<ServerProvider>();
late S s;
static const summaryTextStyle = TextStyle(
fontSize: 12,
@@ -43,27 +50,35 @@ class _PingPageState extends State<PingPage>
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: GestureDetector(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: Column(children: [
const SizedBox(height: 13),
buildInput(context, _textEditingController,
maxLines: 1, onSubmitted: (_) => doPing()),
_buildControl(),
SizedBox(
width: double.infinity,
height: _media.size.height * 0.6,
child: ListView.builder(
controller: ScrollController(),
itemCount: _results.length,
itemBuilder: (context, index) {
final result = _results[index];
return _buildResultItem(result);
}),
),
])),
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: Column(children: [
const SizedBox(height: 13),
buildInput(context, _textEditingController,
hint: s.inputDomainHere,
maxLines: 1,
onSubmitted: (_) => doPing()),
SizedBox(
width: double.infinity,
height: _media.size.height * 0.6,
child: ListView.builder(
controller: ScrollController(),
itemCount: _results.length,
itemBuilder: (context, index) {
final result = _results[index];
return _buildResultItem(result);
}),
),
])),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () {
try {
doPing();
} catch (e) {
showSnackBar(context, Text('Error: \n$e'));
}
},
),
);
}
@@ -106,67 +121,30 @@ class _PingPageState extends State<PingPage>
return;
}
await Future.wait(locator<ServerProvider>().servers.map((e) async {
if (e.client == null) {
return;
}
final result = await e.client!.run('ping -c 3 $target').string;
_results.add(PingResult.parse(e.info.name, result));
setState(() {});
}));
}
if (_serverProvider.servers.isEmpty) {
showSnackBar(context, Text(s.pingNoServer));
return;
}
Widget _buildControl() {
return SizedBox(
height: 57,
child: RoundRectCard(
InkWell(
onTap: () => FocusScope.of(context).unfocus(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Row(
children: [
const Icon(Icons.delete),
const SizedBox(
width: 7,
),
Text(s.clear)
],
),
onPressed: () {
_results.clear();
setState(() {});
},
),
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Row(
children: [
const Icon(Icons.play_arrow),
const SizedBox(
width: 7,
),
Text(s.start)
],
),
onPressed: () {
try {
doPing();
} catch (e) {
showSnackBar(context, Text('Error: \n$e'));
}
},
)
],
),
),
),
);
if (!doaminReg.hasMatch(target) &&
!ipv4Reg.hasMatch(target) &&
!ipv6Reg.hasMatch(target)) {
showSnackBar(context, Text(s.pingInputIP));
return;
}
try {
await Future.wait(_serverProvider.servers.map((e) async {
if (e.client == null) {
return;
}
final result = await e.client!.run('ping -c 3 $target').string;
_results.add(PingResult.parse(e.info.name, result));
setState(() {});
}));
} catch (e) {
showSnackBar(context, Text(e.toString()));
}
}
@override

View File

@@ -24,6 +24,11 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
final nameController = TextEditingController();
final keyController = TextEditingController();
final pwdController = TextEditingController();
final nameNode = FocusNode();
final keyNode = FocusNode();
final pwdNode = FocusNode();
late FocusScopeNode focusScope;
late PrivateKeyProvider _provider;
late Widget loading;
@@ -40,6 +45,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
focusScope = FocusScope.of(context);
}
@override
@@ -62,6 +68,8 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
TextField(
controller: nameController,
keyboardType: TextInputType.text,
focusNode: nameNode,
onSubmitted: (_) => focusScope.requestFocus(keyNode),
decoration: buildDecoration(s.name, icon: Icons.info),
),
TextField(
@@ -70,6 +78,8 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
minLines: 3,
maxLines: 10,
keyboardType: TextInputType.text,
focusNode: keyNode,
onSubmitted: (_) => focusScope.requestFocus(pwdNode),
enableSuggestions: false,
decoration: buildDecoration(s.privateKey, icon: Icons.vpn_key),
),
@@ -77,6 +87,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
controller: pwdController,
autocorrect: false,
keyboardType: TextInputType.text,
focusNode: pwdNode,
obscureText: true,
decoration: buildDecoration(s.pwd, icon: Icons.password),
),
@@ -88,7 +99,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
tooltip: s.save,
onPressed: () async {
final name = nameController.text;
final key = keyController.text;
final key = keyController.text.trim();
final pwd = pwdController.text;
if (name.isEmpty || key.isEmpty) {
showSnackBar(context, Text(s.fieldMustNotEmpty));

View File

@@ -68,8 +68,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildLinuxIcon(String sysVer) {
final iconPath = linuxIcons.search(sysVer);
if (iconPath == null) return const SizedBox();
return SizedBox(
height: _media.size.height * 0.15, child: Image.asset(iconPath));
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: _media.size.height * 0.12,
maxWidth: _media.size.width * 0.6),
child: Image.asset(iconPath),
);
}
Widget _buildCPUView(ServerStatus ss) {
@@ -100,7 +104,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
SizedBox(
width: _media.size.width * 0.03,
),
_buildCPUTimePercent(ss.cpu2Status.nice, 'nice'),
_buildCPUTimePercent(ss.cpu2Status.iowait, 'io'),
SizedBox(
width: _media.size.width * 0.03,
),
@@ -155,6 +159,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildProgress(double percent) {
if (percent > 100) percent = 100;
final pColor = primaryColor;
final percentWithinOne = percent / 100;
return LinearProgressIndicator(
@@ -186,7 +191,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
final pColor = primaryColor;
final used = ss.memory.used / ss.memory.total;
final width = _media.size.width - 17 * 2 - 17 * 2;
const mb = 1024 * 1024;
const mb = 1024;
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: SizedBox(
@@ -329,8 +334,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Icons.device_hub,
size: 17,
),
Icon(Icons.arrow_downward, size: 17),
Icon(Icons.arrow_upward, size: 17),
Icon(Icons.arrow_downward, size: 17)
],
),
);

View File

@@ -31,6 +31,12 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final keyController = TextEditingController();
final nameFocus = FocusNode();
final ipFocus = FocusNode();
final portFocus = FocusNode();
final usernameFocus = FocusNode();
late FocusScopeNode focusScope;
late ServerProvider _serverProvider;
@@ -38,7 +44,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
bool usePublicKey = false;
int _pubKeyIndex = -1;
int? _pubKeyIndex;
PrivateKeyInfo? _keyInfo;
@override
@@ -51,6 +57,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
focusScope = FocusScope.of(context);
}
@override
@@ -89,12 +96,16 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
TextField(
controller: nameController,
keyboardType: TextInputType.text,
focusNode: nameFocus,
onSubmitted: (_) => focusScope.requestFocus(ipFocus),
decoration: buildDecoration(s.name,
icon: Icons.info, hint: s.exampleName),
),
TextField(
controller: ipController,
keyboardType: TextInputType.text,
onSubmitted: (_) => focusScope.requestFocus(portFocus),
focusNode: ipFocus,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration(s.host,
@@ -103,12 +114,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
TextField(
controller: portController,
keyboardType: TextInputType.number,
focusNode: portFocus,
onSubmitted: (_) => focusScope.requestFocus(usernameFocus),
decoration: buildDecoration(s.port,
icon: Icons.format_list_numbered, hint: '22'),
),
TextField(
controller: usernameController,
keyboardType: TextInputType.text,
focusNode: usernameFocus,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration(s.user,
@@ -137,7 +151,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
? Consumer<PrivateKeyProvider>(builder: (_, key, __) {
for (var item in key.infos) {
if (item.id == widget.spi?.pubKeyId) {
_pubKeyIndex = key.infos.indexOf(item);
_pubKeyIndex ??= key.infos.indexOf(item);
}
}
final tiles = key.infos
@@ -176,14 +190,28 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.send),
onPressed: () {
onPressed: () async {
if (ipController.text == '') {
showSnackBar(context, Text(s.plzEnterHost));
return;
}
if (!usePublicKey && passwordController.text == '') {
showSnackBar(context, Text(s.plzEnterPwd));
return;
final cancel = await showRoundDialog<bool>(
context,
s.attention,
Text(s.sureNoPwd),
[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(s.ok)),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(s.cancel))
],
barrierDismiss: false);
if (cancel ?? true) {
return;
}
}
if (usePublicKey && _pubKeyIndex == -1) {
showSnackBar(context, Text(s.plzSelectKey));
@@ -200,18 +228,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_keyInfo ??= locator<PrivateKeyStore>().get(widget.spi!.pubKeyId!);
}
final authorization = usePublicKey
? {
"privateKey": _keyInfo!.privateKey,
"passphrase": _keyInfo!.password
}
: passwordController.text;
final authorization = passwordController.text;
final spi = ServerPrivateInfo(
name: nameController.text,
ip: ipController.text,
port: int.parse(portController.text),
user: usernameController.text,
authorization: authorization,
pwd: authorization,
pubKeyId: usePublicKey ? _keyInfo!.id : null);
if (widget.spi == null) {
@@ -246,12 +269,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
ipController.text = widget.spi?.ip ?? '';
portController.text = (widget.spi?.port ?? 22).toString();
usernameController.text = widget.spi?.user ?? '';
if (widget.spi?.authorization is String) {
passwordController.text = widget.spi?.authorization as String? ?? '';
if (widget.spi?.pubKeyId == null) {
passwordController.text = widget.spi?.pwd ?? '';
} else {
final auth = widget.spi?.authorization as Map;
passwordController.text = auth['passphrase'];
keyController.text = auth['privateKey'];
usePublicKey = true;
}
setState(() {});

View File

@@ -1,15 +1,12 @@
import 'package:after_layout/after_layout.dart';
import 'package:circle_chart/circle_chart.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:marquee/marquee.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/utils.dart';
import 'package:toolbox/data/model/app/menu_item.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_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart';
@@ -37,16 +34,17 @@ class _ServerPageState extends State<ServerPage>
late MediaQueryData _media;
late ThemeData _theme;
late Color _primaryColor;
late RefreshController _refreshController;
late ServerProvider _serverProvider;
late S s;
late bool autoUpdate;
@override
void initState() {
super.initState();
_serverProvider = locator<ServerProvider>();
_refreshController = RefreshController();
autoUpdate =
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
}
@override
@@ -61,8 +59,6 @@ class _ServerPageState extends State<ServerPage>
@override
Widget build(BuildContext context) {
super.build(context);
final autoUpdate =
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return Center(
@@ -90,13 +86,9 @@ class _ServerPageState extends State<ServerPage>
return Scaffold(
body: autoUpdate
? child
: SmartRefresher(
controller: _refreshController,
: RefreshIndicator(
child: child,
onRefresh: () async {
await _serverProvider.refreshData();
_refreshController.refreshCompleted();
},
onRefresh: () async => _serverProvider.refreshData(),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
@@ -167,19 +159,10 @@ class _ServerPageState extends State<ServerPage>
Row(
children: [
hasError
? ConstrainedBox(
constraints: BoxConstraints(
maxWidth: _media.size.width * 0.57,
maxHeight: 15),
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),
)
? GestureDetector(
onTap: () => showRoundDialog(
context, s.error, Text(ss.failedInfo ?? ''), []),
child: Text(s.clickSee, style: style))
: Text(topRightStr, style: style, textScaleFactor: 1.0),
_buildMoreBtn(spi),
],
@@ -216,72 +199,42 @@ class _ServerPageState extends State<ServerPage>
}
Widget _buildMoreBtn(ServerPrivateInfo spi) {
return DropdownButtonHideUnderline(
child: DropdownButton2(
customButton: const Padding(
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 1.7),
child: Icon(
Icons.more_vert,
size: 17,
return buildPopuopMenu(
items: <PopupMenuEntry>[
...ServerTabMenuItems.firstItems.map(
(item) => PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
),
customItemsIndexes: [ServerTabMenuItems.firstItems.length],
customItemsHeight: 8,
items: [
...ServerTabMenuItems.firstItems.map(
(item) => DropdownMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
const PopupMenuDivider(height: 1),
...ServerTabMenuItems.secondItems.map(
(item) => PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
const DropdownMenuItem<Divider>(enabled: false, child: Divider()),
...ServerTabMenuItems.secondItems.map(
(item) => DropdownMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
),
],
onChanged: (value) {
final item = value as DropdownBtnItem;
switch (item) {
case ServerTabMenuItems.apt:
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
break;
case ServerTabMenuItems.sftp:
AppRoute(SFTPPage(spi), 'SFTP').go(context);
break;
case ServerTabMenuItems.snippet:
AppRoute(
SnippetListPage(
spi: spi,
),
'snippet list')
.go(context);
break;
case ServerTabMenuItems.edit:
AppRoute(
ServerEditPage(
spi: spi,
),
'Edit server info page')
.go(context);
break;
case ServerTabMenuItems.docker:
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
break;
}
},
itemHeight: 37,
itemPadding: const EdgeInsets.only(left: 17, right: 17),
dropdownWidth: 160,
dropdownPadding: const EdgeInsets.symmetric(vertical: 7),
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
),
dropdownElevation: 8,
offset: const Offset(0, 8),
),
],
onSelected: (value) {
final item = value as DropdownBtnItem;
switch (item) {
case ServerTabMenuItems.apt:
AppRoute(AptManagePage(spi), 'apt manage').go(context);
break;
case ServerTabMenuItems.sftp:
AppRoute(SFTPPage(spi), 'SFTP').go(context);
break;
case ServerTabMenuItems.snippet:
AppRoute(SnippetListPage(spi: spi), 'snippet list').go(context);
break;
case ServerTabMenuItems.edit:
AppRoute(ServerEditPage(spi: spi), 'Edit server info').go(context);
break;
case ServerTabMenuItems.docker:
AppRoute(DockerManagePage(spi), 'Docker manage').go(context);
break;
}
},
);
}

View File

@@ -6,7 +6,7 @@ import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/absolute_path.dart';
import 'package:toolbox/data/model/sftp/download_worker.dart';

View File

@@ -21,6 +21,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
with AfterLayoutMixin {
final nameController = TextEditingController();
final scriptController = TextEditingController();
final scriptNode = FocusNode();
late SnippetProvider _provider;
late S s;
@@ -57,11 +58,13 @@ class _SnippetEditPageState extends State<SnippetEditPage>
TextField(
controller: nameController,
keyboardType: TextInputType.text,
onSubmitted: (_) => FocusScope.of(context).requestFocus(scriptNode),
decoration: buildDecoration(s.name, icon: Icons.info),
),
TextField(
controller: scriptController,
autocorrect: false,
focusNode: scriptNode,
minLines: 3,
maxLines: 10,
keyboardType: TextInputType.text,

View File

@@ -1,4 +1,3 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
@@ -26,9 +25,6 @@ class SnippetListPage extends StatefulWidget {
class _SnippetListPageState extends State<SnippetListPage> {
late ServerPrivateInfo _selectedIndex;
final _importFieldController = TextEditingController();
final _exportFieldController = TextEditingController();
final _textStyle = TextStyle(color: primaryColor);
late S s;
@@ -44,12 +40,6 @@ class _SnippetListPageState extends State<SnippetListPage> {
return Scaffold(
appBar: AppBar(
title: Text(s.snippet, style: size18),
actions: [
IconButton(
onPressed: () => _showImportExport(),
tooltip: s.importAndExport,
icon: const Icon(Icons.import_export)),
],
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
@@ -60,97 +50,6 @@ class _SnippetListPageState extends State<SnippetListPage> {
);
}
Future<void> _showImportExport() async {
await showRoundDialog(
context,
s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(s.import),
leading: const Icon(Icons.download),
onTap: () => _showImportDialog(),
),
ListTile(
title: Text(s.export),
leading: const Icon(Icons.file_upload),
onTap: () => _showExportDialog(),
),
],
),
[]);
}
Future<void> _showExportDialog() async {
Navigator.of(context).pop();
_exportFieldController.text = locator<SnippetProvider>().export;
await showRoundDialog(
context,
s.export,
TextField(
decoration: const InputDecoration(
labelText: 'JSON',
),
maxLines: 3,
controller: _exportFieldController,
),
[
TextButton(
child: Text(s.ok),
onPressed: () => Navigator.pop(context),
),
]);
}
Future<void> _showImportDialog() async {
Navigator.of(context).pop();
await showRoundDialog(
context,
s.import,
TextField(
decoration: InputDecoration(
labelText: s.urlOrJson,
),
maxLines: 2,
controller: _importFieldController,
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async =>
await _import(_importFieldController.text.trim()),
child: const Text('GO'),
)
]);
}
Future<void> _import(String text) async {
if (text.isEmpty) {
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
final snippetProvider = locator<SnippetProvider>();
if (text.startsWith('http')) {
final resp = await Dio().get(text);
if (resp.statusCode != 200) {
showSnackBar(
context, Text(s.httpFailedWithCode(resp.statusCode ?? '-1')));
return;
}
for (final snippet in getSnippetList(resp.data)) {
snippetProvider.add(snippet);
}
} else {
for (final snippet in getSnippetList(text)) {
snippetProvider.add(snippet);
}
}
Navigator.of(context).pop();
}
Widget _buildBody() {
return Consumer<SnippetProvider>(
builder: (_, key, __) {

View File

@@ -24,7 +24,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3

View File

@@ -420,14 +420,14 @@
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 159;
DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.136;
MARKETING_VERSION = 1.0.159;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@@ -550,14 +550,14 @@
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 159;
DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.136;
MARKETING_VERSION = 1.0.159;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -574,14 +574,14 @@
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 159;
DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.136;
MARKETING_VERSION = 1.0.159;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;

View File

@@ -104,8 +104,9 @@ Future<void> flutterBuild(
buildType,
];
// No sksl cache for macos
if ('macos' != buildType) {
args.add('--bundle-sksl-path=$buildType$skslFileSuffix');
final skslPath = '$buildType$skslFileSuffix';
if (await File(skslPath).exists()) {
args.add('--bundle-sksl-path=$skslPath');
}
final isAndroid = 'apk' == buildType;
// [--target-platform] only for Android
@@ -116,7 +117,7 @@ Future<void> flutterBuild(
'--build-name=1.0.$build',
]);
}
print('Building with args: ${args.join(' ')}');
print('[$buildType]\nBuilding with args: ${args.join(' ')}');
final buildResult = await Process.run('flutter', args, runInShell: true);
final exitCode = buildResult.exitCode;
@@ -135,7 +136,8 @@ Future<void> flutterBuild(
print('Done.\n');
} else {
print(buildResult.stderr.toString());
print(buildResult.stdout);
print(buildResult.stderr);
print('\nBuild failed with exit code $exitCode');
exit(exitCode);
}

View File

@@ -1,13 +1,27 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "50.0.0"
after_layout:
dependency: "direct main"
description:
name: after_layout
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.0"
archive:
dependency: transitive
description:
@@ -35,7 +49,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.2"
version: "2.9.0"
boolean_selector:
dependency: transitive
description:
@@ -43,20 +57,76 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.1"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
build_daemon:
dependency: transitive
description:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.2"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "7.2.7"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.4.2"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
charcode:
version: "1.2.1"
checked_yaml:
dependency: transitive
description:
name: charcode
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
version: "2.0.1"
circle_chart:
dependency: "direct main"
description:
@@ -66,20 +136,20 @@ packages:
url: "https://github.com/LollipopKit/circle_chart"
source: git
version: "0.0.3"
clipboard:
dependency: "direct main"
description:
name: clipboard
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.0"
collection:
dependency: transitive
description:
@@ -117,13 +187,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.4"
dartssh2:
dependency: "direct main"
description:
name: dartssh2
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.1"
version: "2.7.1"
dio:
dependency: "direct main"
description:
@@ -131,13 +208,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.6"
dropdown_button2:
dependency: "direct main"
description:
name: dropdown_button2
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
easy_isolate:
dependency: "direct main"
description:
@@ -151,7 +221,7 @@ packages:
name: extended_image
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.0"
version: "6.2.1"
extended_image_library:
dependency: transitive
description:
@@ -159,27 +229,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
fading_edge_scrollview:
dependency: transitive
description:
name: fading_edge_scrollview
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
version: "1.2.1"
file:
dependency: transitive
description:
@@ -187,18 +250,18 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_advanced_drawer:
dependency: "direct main"
description:
name: flutter_advanced_drawer
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.2"
flutter_lints:
dependency: "direct dev"
description:
@@ -224,7 +287,7 @@ packages:
name: flutter_native_splash
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.6"
version: "2.2.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -242,6 +305,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
get_it:
dependency: "direct main"
description:
@@ -249,13 +319,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "7.2.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
graphs:
dependency: transitive
description:
name: graphs
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
hive:
dependency: "direct main"
dependency: transitive
description:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.1"
hive_flutter:
dependency: "direct main"
description:
@@ -263,6 +347,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
hive_generator:
dependency: "direct dev"
description:
name: hive_generator
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
http:
dependency: transitive
description:
@@ -277,20 +368,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
version: "4.0.1"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
version: "3.2.0"
intl:
dependency: "direct main"
description:
@@ -298,6 +396,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
js:
dependency: transitive
description:
@@ -305,6 +410,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.7.0"
lint:
dependency: transitive
description:
@@ -326,34 +438,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
marquee:
dependency: "direct main"
description:
name: marquee
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
version: "0.12.12"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
mime:
dependency: transitive
description:
@@ -368,13 +473,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
path_provider:
dependency: "direct main"
description:
@@ -466,6 +578,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.0"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.1"
process:
dependency: transitive
description:
@@ -479,21 +598,28 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.2"
pull_to_refresh:
dependency: "direct main"
version: "6.0.3"
pub_semver:
dependency: transitive
description:
name: pull_to_refresh
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.2"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
r_upgrade:
dependency: "direct main"
description:
name: r_upgrade
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.8+1"
version: "0.3.8+2"
share_plus:
dependency: "direct main"
description:
@@ -536,18 +662,46 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.6"
source_helper:
dependency: transitive
description:
name: source_helper
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.3"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
version: "1.9.0"
stack_trace:
dependency: transitive
description:
@@ -562,34 +716,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
stream_transform:
dependency: transitive
description:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
version: "0.4.12"
timing:
dependency: transitive
description:
name: timing
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
universal_io:
dependency: transitive
description:
@@ -617,7 +785,7 @@ packages:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.16"
version: "6.0.17"
url_launcher_linux:
dependency: transitive
description:
@@ -660,13 +828,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.2"
version: "2.6.1"
xdg_directories:
dependency: transitive
description:
@@ -680,14 +862,14 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.4.1"
version: "6.0.1"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "3.1.1"
sdks:
dart: ">=2.17.0 <3.0.0"
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.0.0"

View File

@@ -33,7 +33,6 @@ dependencies:
sdk: flutter
provider: ^6.0.0
get_it: ^7.2.0
hive: ^2.0.0
hive_flutter: ^1.0.0
dio: ^4.0.0
after_layout: ^1.1.0
@@ -43,7 +42,7 @@ dependencies:
git:
url: https://github.com/Countly/countly-sdk-flutter-bridge.git
ref: master
dartssh2: ^2.3.1-pre
dartssh2: ^2.7.1
logging: ^1.0.2
flutter_material_color_picker: ^1.1.0+2
circle_chart:
@@ -51,12 +50,7 @@ dependencies:
url: https://github.com/LollipopKit/circle_chart
ref: main
# path: ../circle_chart
clipboard: ^0.1.3
r_upgrade: ^0.3.6
pull_to_refresh: ^2.0.0
marquee: ^2.2.0
dropdown_button2: ^1.1.1
flutter_advanced_drawer: ^1.3.0
path_provider: ^2.0.9
easy_isolate: ^1.3.0
share_plus: ^4.0.4
@@ -64,6 +58,8 @@ dependencies:
dev_dependencies:
flutter_native_splash: ^2.1.6
hive_generator: ^2.0.0
build_runner: ^2.3.2
flutter_test:
sdk: flutter
@@ -94,6 +90,8 @@ flutter:
- assets/linux/arch.png
- assets/linux/fedora.png
- assets/linux/opensuse.png
- assets/linux/wrt.png
- assets/linux/armbian.png
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see

6
screenshots/appstore.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="360" height="120" viewBox="0 0 360 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 16C0 7.16344 7.16344 0 16 0H344C352.837 0 360 7.16344 360 16V104C360 112.837 352.837 120 344 120H16C7.16344 120 0 112.837 0 104V16Z" fill="black"/>
<path d="M1.50391 5.49902V21H6.71387C11.3223 21 14.0293 18.1318 14.0293 13.2441C14.0293 8.36719 11.3115 5.49902 6.71387 5.49902H1.50391ZM3.4375 7.23926H6.58496C10.1084 7.23926 12.0527 9.3877 12.0527 13.2656C12.0527 17.1113 10.0977 19.2598 6.58496 19.2598H3.4375V7.23926ZM20.7754 21.1934C23.8584 21.1934 25.9424 18.9697 25.9424 15.4141C25.9424 11.8477 23.8584 9.63477 20.7754 9.63477C17.6816 9.63477 15.5977 11.8477 15.5977 15.4141C15.5977 18.9697 17.6816 21.1934 20.7754 21.1934ZM20.7754 19.5498C18.7988 19.5498 17.499 18.0459 17.499 15.4141C17.499 12.7822 18.7988 11.2783 20.7754 11.2783C22.752 11.2783 24.041 12.7822 24.041 15.4141C24.041 18.0459 22.752 19.5498 20.7754 19.5498ZM42.0449 9.82812H40.1973L37.9629 18.916H37.9199L35.3848 9.82812H33.5801L31.0557 18.916H31.0127L28.7783 9.82812H26.9199L30.0459 21H31.9473L34.4609 12.3525H34.5039L37.0283 21H38.9297L42.0449 9.82812ZM43.5811 21H45.4502V14.4258C45.4502 12.5352 46.5996 11.2891 48.3613 11.2891C50.0801 11.2891 50.9287 12.2773 50.9287 14.082V21H52.7979V13.749C52.7979 11.2139 51.3799 9.63477 48.9307 9.63477C47.2012 9.63477 46.0088 10.4082 45.3857 11.6113H45.3428V9.82812H43.5811V21ZM55.2793 21H57.1484V5.49902H55.2793V21ZM64.3027 21.1934C67.3857 21.1934 69.4697 18.9697 69.4697 15.4141C69.4697 11.8477 67.3857 9.63477 64.3027 9.63477C61.209 9.63477 59.125 11.8477 59.125 15.4141C59.125 18.9697 61.209 21.1934 64.3027 21.1934ZM64.3027 19.5498C62.3262 19.5498 61.0264 18.0459 61.0264 15.4141C61.0264 12.7822 62.3262 11.2783 64.3027 11.2783C66.2793 11.2783 67.5684 12.7822 67.5684 15.4141C67.5684 18.0459 66.2793 19.5498 64.3027 19.5498ZM75.0449 19.6035C73.7021 19.6035 72.8105 18.9053 72.8105 17.8418C72.8105 16.7891 73.6699 16.123 75.1738 16.0264L78.1602 15.8438V16.7568C78.1602 18.3682 76.8066 19.6035 75.0449 19.6035ZM74.626 21.1934C76.1299 21.1934 77.5479 20.3555 78.2139 19.0771H78.2568V21H80.0293V13.2764C80.0293 11.0527 78.375 9.63477 75.7539 9.63477C73.1113 9.63477 71.457 11.1172 71.3281 13.0508H73.1328C73.3691 11.9551 74.2715 11.2783 75.7109 11.2783C77.2578 11.2783 78.1602 12.0947 78.1602 13.459V14.3936L74.959 14.5762C72.3809 14.7266 70.9092 15.9297 70.9092 17.8418C70.9092 19.8506 72.4023 21.1934 74.626 21.1934ZM86.6357 21.1934C88.2471 21.1934 89.5254 20.3984 90.2451 19.1094H90.2773V21H92.0605V5.49902H90.1914V11.6436H90.1484C89.4824 10.4189 88.1934 9.63477 86.5928 9.63477C83.7783 9.63477 81.8555 11.9121 81.8555 15.4141C81.8555 18.916 83.7891 21.1934 86.6357 21.1934ZM86.9902 11.2783C88.9131 11.2783 90.2021 12.9219 90.2021 15.4141C90.2021 17.917 88.9131 19.5498 86.9902 19.5498C85.0137 19.5498 83.7676 17.9492 83.7676 15.4141C83.7676 12.8896 85.0137 11.2783 86.9902 11.2783ZM103.727 21.1934C106.81 21.1934 108.894 18.9697 108.894 15.4141C108.894 11.8477 106.81 9.63477 103.727 9.63477C100.633 9.63477 98.5488 11.8477 98.5488 15.4141C98.5488 18.9697 100.633 21.1934 103.727 21.1934ZM103.727 19.5498C101.75 19.5498 100.45 18.0459 100.45 15.4141C100.45 12.7822 101.75 11.2783 103.727 11.2783C105.703 11.2783 106.992 12.7822 106.992 15.4141C106.992 18.0459 105.703 19.5498 103.727 19.5498ZM110.784 21H112.653V14.4258C112.653 12.5352 113.803 11.2891 115.564 11.2891C117.283 11.2891 118.132 12.2773 118.132 14.082V21H120.001V13.749C120.001 11.2139 118.583 9.63477 116.134 9.63477C114.404 9.63477 113.212 10.4082 112.589 11.6113H112.546V9.82812H110.784V21ZM127.499 7.13184V9.82812H125.834V11.3643H127.499V18.0996C127.499 20.248 128.294 21.0859 130.346 21.0859C130.786 21.0859 131.302 21.0537 131.527 21V19.4316C131.398 19.4531 130.969 19.4854 130.754 19.4854C129.798 19.4854 129.368 19.0234 129.368 18.0029V11.3643H131.538V9.82812H129.368V7.13184H127.499ZM133.633 21H135.502V14.4902C135.502 12.5566 136.651 11.2891 138.51 11.2891C140.261 11.2891 141.152 12.3525 141.152 14.125V21H143.021V13.7812C143.021 11.2891 141.539 9.63477 139.09 9.63477C137.393 9.63477 136.168 10.3652 135.545 11.6113H135.502V5.49902H133.633V21ZM149.875 11.2568C151.637 11.2568 152.808 12.5889 152.851 14.458H146.781C146.899 12.5996 148.124 11.2568 149.875 11.2568ZM152.808 17.7988C152.496 18.8516 151.454 19.5713 150.036 19.5713C148.027 19.5713 146.771 18.1641 146.771 16.0264V15.9189H154.784V15.21C154.784 11.8262 152.926 9.63477 149.907 9.63477C146.824 9.63477 144.848 11.9873 144.848 15.457C144.848 18.9697 146.792 21.1934 149.993 21.1934C152.432 21.1934 154.322 19.7539 154.645 17.7988H152.808Z" transform="translate(112 21)" fill="white"/>
<path d="M24.4694 44H29.7926L18.0905 11.5889H12.6549L0.952774 44H6.18617L9.15102 35.2402H21.5045L24.4694 44ZM15.2604 16.9795H15.3952L20.2243 31.1973H10.4313L15.2604 16.9795ZM46.0085 19.9893C42.5945 19.9893 39.8767 21.7188 38.4841 24.4365H38.3943V20.3711H33.6999V51.8164H38.5515V40.1143H38.6638C40.0114 42.7422 42.7068 44.4043 46.0984 44.4043C52.028 44.4043 55.9587 39.665 55.9587 32.1855C55.9587 24.7061 52.0056 19.9893 46.0085 19.9893ZM44.7282 40.2939C40.9997 40.2939 38.529 37.1045 38.529 32.1855C38.529 27.3115 41.0222 24.0771 44.7282 24.0771C48.5691 24.0771 50.9724 27.2441 50.9724 32.1855C50.9724 37.1494 48.5691 40.2939 44.7282 40.2939ZM72.9383 19.9893C69.5242 19.9893 66.8064 21.7188 65.4139 24.4365H65.324V20.3711H60.6297V51.8164H65.4813V40.1143H65.5936C66.9412 42.7422 69.6365 44.4043 73.0281 44.4043C78.9578 44.4043 82.8885 39.665 82.8885 32.1855C82.8885 24.7061 78.9354 19.9893 72.9383 19.9893ZM71.658 40.2939C67.9295 40.2939 65.4588 37.1045 65.4588 32.1855C65.4588 27.3115 67.952 24.0771 71.658 24.0771C75.4988 24.0771 77.9021 27.2441 77.9021 32.1855C77.9021 37.1494 75.4988 40.2939 71.658 40.2939ZM96.9697 34.9707C97.2841 40.833 102.136 44.5391 109.323 44.5391C117.005 44.5391 121.834 40.7432 121.834 34.6787C121.834 29.9395 119.071 27.2666 112.423 25.6943L108.852 24.8184C104.674 23.7852 102.967 22.4375 102.967 20.1465C102.967 17.2715 105.64 15.2949 109.548 15.2949C113.456 15.2949 116.129 17.249 116.421 20.4834H121.317C121.16 14.8906 116.466 11.0498 109.615 11.0498C102.697 11.0498 97.8232 14.8906 97.8232 20.4834C97.8232 24.998 100.586 27.7832 106.538 29.2207L110.738 30.2314C114.938 31.2646 116.713 32.7695 116.713 35.2852C116.713 38.1826 113.77 40.2939 109.683 40.2939C105.415 40.2939 102.293 38.1826 101.934 34.9707H96.9697ZM128.1 14.7783V20.3711H124.663V24.2119H128.1V37.7783C128.1 42.3379 129.964 44.1572 134.748 44.1572C135.826 44.1572 136.904 44.0674 137.443 43.9326V40.0918C137.129 40.1592 136.343 40.2041 135.849 40.2041C133.872 40.2041 132.974 39.2832 132.974 37.2393V24.2119H137.466V20.3711H132.974V14.7783H128.1ZM151.952 44.4492C158.713 44.4492 163.16 39.7998 163.16 32.1855C163.16 24.5938 158.691 19.9443 151.952 19.9443C145.214 19.9443 140.744 24.5938 140.744 32.1855C140.744 39.7998 145.192 44.4492 151.952 44.4492ZM151.952 40.4512C148.156 40.4512 145.686 37.4414 145.686 32.1855C145.686 26.9521 148.156 23.9424 151.952 23.9424C155.748 23.9424 158.219 26.9521 158.219 32.1855C158.219 37.4414 155.771 40.4512 151.952 40.4512ZM167.831 44H172.683V29.8496C172.683 26.5029 174.614 24.4141 177.737 24.4141C178.68 24.4141 179.533 24.5264 179.915 24.6836V20.1689C179.556 20.1016 178.949 19.9893 178.253 19.9893C175.468 19.9893 173.357 21.6289 172.571 24.3916H172.458V20.3711H167.831V44ZM192.695 23.8525C196.086 23.8525 198.377 26.3906 198.49 30.0068H186.675C186.922 26.4355 189.303 23.8525 192.695 23.8525ZM198.467 37.0596C197.816 39.126 195.772 40.5186 193.009 40.5186C189.123 40.5186 186.653 37.8008 186.653 33.6904V33.3984H203.341V31.7363C203.341 24.5713 199.253 19.9443 192.695 19.9443C186.024 19.9443 181.756 24.8857 181.756 32.2979C181.756 39.7549 185.956 44.4492 192.897 44.4492C198.31 44.4492 202.398 41.3721 203.072 37.0596H198.467Z" transform="translate(112 46)" fill="white"/>
<path d="M58.7425 55.4758C57.6652 57.9629 56.3901 60.2522 54.9128 62.357C52.899 65.2263 51.2502 67.2125 49.9795 68.3154C48.0097 70.1257 45.8992 71.0529 43.6392 71.1056C42.0167 71.1056 40.0601 70.6443 37.7825 69.7083C35.4974 68.7768 33.3975 68.3154 31.4774 68.3154C29.4636 68.3154 27.3038 68.7768 24.9937 69.7083C22.6801 70.6443 20.8162 71.132 19.3912 71.1803C17.2239 71.2726 15.0638 70.3191 12.9075 68.3154C11.5313 67.1158 9.80991 65.0594 7.74776 62.1461C5.53525 59.0351 3.71625 55.4275 2.29122 51.3146C0.765059 46.8722 0 42.5704 0 38.4057C0 33.635 1.03151 29.5204 3.09761 26.0723C4.72138 23.3027 6.88158 21.118 9.58523 19.5142C12.2889 17.9103 15.2102 17.093 18.3561 17.0407C20.0775 17.0407 22.3349 17.5729 25.1401 18.6186C27.9374 19.668 29.7335 20.2001 30.521 20.2001C31.1098 20.2001 33.1051 19.5779 36.4876 18.3374C39.6864 17.187 42.386 16.7107 44.5977 16.8984C50.5906 17.3817 55.0931 19.7427 58.0873 23.9961C52.7275 27.2416 50.0762 31.7873 50.129 37.6187C50.1773 42.1609 51.8262 45.9407 55.0667 48.9418C56.5352 50.3348 58.1753 51.4113 60 52.1759C59.6043 53.3227 59.1866 54.4213 58.7425 55.4758ZM44.9978 1.42413C44.9978 4.98426 43.6963 8.30835 41.1022 11.3851C37.9716 15.0427 34.185 17.1563 30.0787 16.8228C30.0264 16.3957 29.996 15.9462 29.996 15.4738C29.996 12.0561 31.4848 8.39843 34.1287 5.40781C35.4486 3.8936 37.1274 2.63457 39.1631 1.63021C41.1945 0.640834 43.1159 0.093682 44.923 0C44.9758 0.475924 44.9978 0.951892 44.9978 1.42408V1.42413Z" transform="translate(26 23)" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
screenshots/apt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

BIN
screenshots/convert.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
screenshots/docker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

BIN
screenshots/ping.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB