Compare commits

...

39 Commits

Author SHA1 Message Date
lollipopkit
5666a23e00 #12 new: custom theme mode 2023-02-25 19:58:37 +08:00
lollipopkit
69fae4dd21 fix: android sftp downloaded files share failed 2023-02-21 17:32:22 +08:00
lollipopkit
2986f80f89 opt: auto rm pwd in key page for safe 2023-02-21 17:12:01 +08:00
lollipopkit
e423e56152 try solve file_picker 2023-02-18 13:18:35 +08:00
lollipopkit
9c00dc8a54 try to solve file_picker on ios 2023-02-17 18:57:16 +08:00
lollipopkit
558721fa79 opt.
opt: check `private key` size
opt: expand key list in default
2023-02-17 16:49:41 +08:00
lollipopkit
0c198c23fc opt: check private key size 2023-02-17 16:29:46 +08:00
lollipopkit
99aa0fc1f5 new: pull to refresh on server tab 2023-02-17 15:55:34 +08:00
lollipopkit
1aac166c43 new: pick from file to add key #9 2023-02-16 12:55:23 +08:00
lollipopkit
28a6067033 new: swap view for #10 2023-02-16 12:19:38 +08:00
lollipopkit
9c3b822311 new & opt
new: Flutter 3.7.3
opt: for `ping` page
2023-02-13 17:51:36 +08:00
lollipopkit
ba44649ce1 fix: #8 2023-02-13 14:49:02 +08:00
lollipopkit
e7b1773e5c opt: settings page 2023-02-06 15:32:36 +08:00
lollipopkit
3feef3936c new & opt
new: support set maxRetryCount of server reconnection
opt: server detail UI
opt: server provider
opt: `ssh` page on Android
2023-02-03 13:12:39 +08:00
lollipopkit
7837fa4339 fix: ssh use SafeArea 2023-02-02 18:47:07 +08:00
lollipopkit
82a201d3dc new: support pick ssh term theme 2023-02-02 16:52:30 +08:00
lollipopkit
c479d18714 new & opt
new: `net` total in & out bytes
opt: i18n for `ssh`
opt: disk path ignore
2023-02-02 13:11:21 +08:00
lollipopkit
469b9fe8cd fix: netspeed bytes too large 2023-02-02 12:04:01 +08:00
lollipopkit
c47e24ac5b ssh: long press menu bar 2023-02-01 23:36:21 +08:00
lollipopkit
1063916474 ssh: support copy/paste, fix ios backspace 2023-02-01 21:34:16 +08:00
lollipopkit
a63e240ce0 fix & opt
fix: whether display docker edit host
opt: docker funcs
2023-02-01 18:24:56 +08:00
lollipopkit
1a8d572fbd fix: btn theme color 2023-02-01 17:48:20 +08:00
lollipopkit
21ac323ed1 fix & opt
fix: cant ping when launch page is ping
fix: button text color not primaryColor
opt: getting primaryColor
2023-02-01 17:18:46 +08:00
lollipopkit
2faea10d61 server connect: max try 7 times 2023-02-01 13:34:45 +08:00
lollipopkit
04cf5b65ce opt. proj struct 2023-02-01 13:00:02 +08:00
lollipopkit
4d741ac82a fix & opt
fix: android in-app upgrade
fmt: proj struct
opt: fetch primaryColor
2023-02-01 12:52:40 +08:00
lollipopkit
068089d207 fix cpu temp padding 2023-02-01 10:37:06 +08:00
lollipopkit
5ebb4e6b3e ssh: more tip 2023-01-29 22:23:18 +08:00
lollipopkit
19e0b283ae ssh page opt: performance & auto exit 2023-01-29 22:03:25 +08:00
lollipopkit
7e8600ab6d fix typo 2023-01-29 17:51:23 +08:00
LollipopKit
7c0e01d0d5 fix & opt.
fix: net iface parse
opt: `ssh` page auto unpress `ctrl or alt` after call
opt: enable translation for menu
opt: add confirm to `docker` page
2023-01-29 17:39:27 +08:00
LollipopKit
f3c670d82c opt. proj struct 2023-01-29 16:56:40 +08:00
lollipopkit
49f9b0b179 ssh page: rm appbar 2023-01-29 15:11:01 +08:00
lollipopkit
47861b1e0b rm: ssh term size 2023-01-29 13:56:39 +08:00
lollipopkit
923667d57c opt. for ssh 2023-01-29 00:09:20 +08:00
lollipopkit
e6458a1d7f opt. for ssh page 2023-01-28 23:39:03 +08:00
lollipopkit
f109aca484 ssh: more virtual keys 2023-01-28 23:10:59 +08:00
lollipopkit
a518dca0ca opt. 2023-01-28 21:16:53 +08:00
lollipopkit
be1a162632 fix & opt. 2023-01-28 15:35:19 +08:00
91 changed files with 3253 additions and 4229 deletions

View File

@@ -4,7 +4,7 @@
# This file should be version controlled.
version:
revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
channel: stable
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
- platform: macos
create_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
base_revision: b06b8b2710955028a6b562f5aa6fe62941d6febf
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
# User provided section

View File

@@ -3,6 +3,9 @@
"files.watcherExclude": {
"**/.fvm": true
},
"git.ignoredRepositories": [
".fvm"
],
"search.exclude": {
"**/.fvm": true
}

View File

@@ -13,6 +13,12 @@
</a>
</p>
<p align="center">
<a href="https://count.ly/f/badge" rel="nofollow">
<img style="height: 37px" src="https://count.ly/badges/light.svg">
</a>
</p>
<p align="center">
A Flutter project which provide charts to display server status and tools to manage server.
<br>
@@ -21,11 +27,18 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
## 🔖 Feature
- [x] 📊 Status chart view
- [x] ⌨️ `SSH` terminal
- [x] ⚙️ `Docker & Pkg` Manager, `SFTP`, `Snippet` ~~market~~, `Ping` and etc.
- [x] 📚 i18n (English, Chinese), **welcome contribution** :)
- [x] 🖥️ Desktop support
- [x] Functions
- [x] `SSH` Terminal
- [x] `Docker & Pkg` Manager
- [x] `SFTP`
- [x] `Snippet`
- [x] `Ping`
- [x] Status charts
- [x] etc.
- [x] i18n (English, Chinese)
- **Welcome contribution** :)
- [How to contribute?](#i18n-guide)
- [x] Desktop support
## 📱 ScreenShots
<table>
@@ -34,11 +47,14 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
<img width="200px" src="screenshots/server.jpg">
</td>
<td>
<img width="200px" src="screenshots/server_detail.png">
<img width="200px" src="screenshots/detail.jpg">
</td>
<td>
<img width="200px" src="screenshots/ssh.jpg">
</td>
<td>
<img width="200px" src="screenshots/apt.png">
</td>
</tr>
</table>
<table>
@@ -52,6 +68,9 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
<td>
<img width="200px" src="screenshots/docker.jpg">
</td>
<td>
<img width="200px" src="screenshots/convert.png">
</td>
</tr>
</table>
@@ -61,6 +80,13 @@ Status|Platform
Full Support|Android/iOS
Support, but not tested|macOS/Windows/Linux
## i18n guide
1. Fork this repo and clone it to your local machine.
2. Create `arb` file in `lib/l10n/` directory
- File name should be `intl_XX.arb`, where `XX` is the language code. Such as `intl_en.arb` for English and `intl_zh.arb` for Chinese.
3. Add content to the file. You can refer to `intl_en.arb` and `intl_zh.arb` for the format.
4. Pull commit to your forked repo.
5. Request a pull request on my repo.
## 📝 License
`GPL v3. lollipopkit 2023`

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,10 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
# Used for flutter lib "file_picker"
Pod::PICKER_MEDIA = false
Pod::PICKER_AUDIO = false
target 'Runner' do
use_frameworks!
use_modular_headers!

View File

@@ -1,6 +1,8 @@
PODS:
- countly_flutter (22.09.0):
- Flutter
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_native_splash (0.0.1):
- Flutter
@@ -16,6 +18,7 @@ PODS:
DEPENDENCIES:
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
@@ -26,6 +29,8 @@ DEPENDENCIES:
EXTERNAL SOURCES:
countly_flutter:
:path: ".symlinks/plugins/countly_flutter/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_native_splash:
@@ -41,13 +46,14 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
countly_flutter: 135f1a4930f8e26ba223a14201d3f265ea7b4c83
file_picker: 1d63c4949e05e386da864365f8c13e1e64787675
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
PODFILE CHECKSUM: 7fb15c416f8685fca4966867a8da218ec592ec2e
COCOAPODS: 1.11.3

View File

@@ -356,7 +356,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 174;
CURRENT_PROJECT_VERSION = 227;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -364,7 +364,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.174;
MARKETING_VERSION = 1.0.227;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -486,7 +486,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 174;
CURRENT_PROJECT_VERSION = 227;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -494,7 +494,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.174;
MARKETING_VERSION = 1.0.227;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -510,7 +510,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 174;
CURRENT_PROJECT_VERSION = 227;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -518,7 +518,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.174;
MARKETING_VERSION = 1.0.227;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -47,8 +47,16 @@
<string>zh</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

File diff suppressed because one or more lines are too long

4
l10n.yaml Normal file
View File

@@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: l10n.dart
output-class: S

View File

@@ -1,79 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:toolbox/core/extension/colorx.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/home.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '/core/extension/colorx.dart';
import 'core/utils/ui.dart';
import 'data/res/build_data.dart';
import 'data/res/color.dart';
import 'data/store/setting.dart';
import 'locator.dart';
import 'view/page/home.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
MyApp({Key? key}) : super(key: key);
final _setting = locator<SettingStore>();
@override
Widget build(BuildContext context) {
setTransparentNavigationBar(context);
return ValueListenableBuilder<int>(
valueListenable: locator<SettingStore>().primaryColor.listenable(),
builder: (_, value, __) {
final primaryColor = Color(value);
final textStyle = TextStyle(color: primaryColor);
final materialColor = primaryColor.materialStateColor;
final materialColorAlpha =
primaryColor.withOpacity(0.7).materialStateColor;
return MaterialApp(
localizationsDelegates: const [
S.delegate,
...GlobalMaterialLocalizations.delegates,
],
supportedLocales: S.delegate.supportedLocales,
title: BuildData.name,
theme: ThemeData(
primaryColor: primaryColor,
appBarTheme: AppBarTheme(backgroundColor: primaryColor),
floatingActionButtonTheme:
FloatingActionButtonThemeData(backgroundColor: primaryColor),
iconTheme: IconThemeData(color: primaryColor),
primaryIconTheme: IconThemeData(color: primaryColor),
switchTheme: SwitchThemeData(
thumbColor: materialColor,
trackColor: materialColorAlpha,
),
buttonTheme: ButtonThemeData(splashColor: primaryColor),
inputDecorationTheme: InputDecorationTheme(
labelStyle: textStyle,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor),
),
),
radioTheme: RadioThemeData(
fillColor: materialColor,
),
),
darkTheme: ThemeData.dark().copyWith(
primaryColor: primaryColor,
floatingActionButtonTheme:
FloatingActionButtonThemeData(backgroundColor: primaryColor),
iconTheme: IconThemeData(color: primaryColor),
primaryIconTheme: IconThemeData(color: primaryColor),
switchTheme: SwitchThemeData(
thumbColor: materialColor,
trackColor: materialColorAlpha,
),
buttonTheme: ButtonThemeData(splashColor: primaryColor),
inputDecorationTheme: InputDecorationTheme(
labelStyle: textStyle,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor),
),
),
radioTheme: RadioThemeData(
fillColor: materialColor,
),
),
home: MyHomePage(primaryColor: primaryColor),
);
});
valueListenable: _setting.primaryColor.listenable(),
builder: (_, colorValue, __) {
primaryColor = Color(colorValue);
return ValueListenableBuilder<int>(
valueListenable: _setting.nightMode.listenable(),
builder: (_, mode, __) => _buildApp(mode),
);
},
);
}
Widget _buildApp(int nightMode) {
final textStyle = TextStyle(color: primaryColor);
final materialColor = primaryColor.materialStateColor;
final materialColorAlpha = primaryColor.withOpacity(0.7).materialStateColor;
final fabTheme =
FloatingActionButtonThemeData(backgroundColor: primaryColor);
final switchTheme = SwitchThemeData(
thumbColor: materialColor,
trackColor: materialColorAlpha,
);
final appBarTheme = AppBarTheme(backgroundColor: primaryColor);
final iconTheme = IconThemeData(color: primaryColor);
final inputDecorationTheme = InputDecorationTheme(
labelStyle: textStyle,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor),
),
);
final radioTheme = RadioThemeData(
fillColor: materialColor,
);
final ok = nightMode >= 0 && nightMode <= ThemeMode.values.length - 1;
final themeMode = ok ? ThemeMode.values[nightMode] : ThemeMode.system;
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: S.localizationsDelegates,
supportedLocales: S.supportedLocales,
title: BuildData.name,
themeMode: themeMode,
theme: ThemeData(
useMaterial3: false,
primaryColor: primaryColor,
primarySwatch: primaryColor.materialColor,
appBarTheme: appBarTheme,
floatingActionButtonTheme: fabTheme,
iconTheme: iconTheme,
primaryIconTheme: iconTheme,
switchTheme: switchTheme,
inputDecorationTheme: inputDecorationTheme,
radioTheme: radioTheme,
),
darkTheme: ThemeData.dark().copyWith(
useMaterial3: false,
floatingActionButtonTheme: fabTheme,
iconTheme: iconTheme,
primaryIconTheme: iconTheme,
switchTheme: switchTheme,
inputDecorationTheme: inputDecorationTheme,
radioTheme: radioTheme,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: primaryColor.materialColor,
brightness: Brightness.dark,
accentColor: primaryColor,
),
),
themeAnimationDuration: const Duration(milliseconds: 237),
home: const MyHomePage(),
);
}
}

View File

@@ -24,4 +24,17 @@ extension ColorX on Color {
return null;
});
}
MaterialColor get materialColor => MaterialColor(value, {
50: withOpacity(0.05),
100: withOpacity(0.1),
200: withOpacity(0.2),
300: withOpacity(0.3),
400: withOpacity(0.4),
500: withOpacity(0.5),
600: withOpacity(0.6),
700: withOpacity(0.7),
800: withOpacity(0.8),
900: withOpacity(0.9),
});
}

View File

@@ -2,14 +2,16 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:logging/logging.dart';
import 'package:r_upgrade/r_upgrade.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import '../data/model/app/update.dart';
import '../data/provider/app.dart';
import '../data/res/build_data.dart';
import '../data/service/app.dart';
import '../locator.dart';
import 'utils/ui.dart';
final _logger = Logger('UPDATE');
@@ -47,29 +49,50 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
_logger.info('Update available: $newest');
if (Platform.isAndroid && !await isFileAvailable(update.android)) {
_logger.warning('Android update file not available');
return;
}
final s = S.of(context);
final s = S.of(context)!;
if (update.min > BuildData.build) {
showRoundDialog(
context,
s.attention,
Text(s.updateTipTooLow(newest)),
[
TextButton(
onPressed: () => _doUpdate(update, context, s),
child: Text(s.ok),
)
],
);
return;
}
showSnackBarWithAction(
context,
update.min > BuildData.build
? 'Your version is too old. \nPlease update to v1.0.$newest.'
: 'Update: v1.0.$newest available. \n${update.changelog}',
s.update, () async {
if (Platform.isAndroid) {
await RUpgrade.upgrade(update.android,
fileName: update.android.split('/').last, isAutoRequestInstall: true);
} else if (Platform.isIOS) {
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))
]);
}
});
context,
'${s.updateTip(newest)} \n${update.changelog}',
s.update,
() => _doUpdate(update, context, s),
);
}
Future<void> _doUpdate(AppUpdate update, BuildContext context, S s) async {
if (Platform.isAndroid) {
await RUpgrade.upgrade(
update.android,
fileName: update.android.split('/').last,
isAutoRequestInstall: true,
);
} else if (Platform.isIOS) {
await RUpgrade.upgradeFromAppStore('1586449703');
} else {
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok),
)
]);
}
}

33
lib/core/utils/misc.dart Normal file
View File

@@ -0,0 +1,33 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:share_plus/share_plus.dart';
Future<bool> shareFiles(BuildContext context, List<String> filePaths) async {
for (final filePath in filePaths) {
if (!await File(filePath).exists()) {
return false;
}
}
var text = '';
if (filePaths.length == 1) {
text = filePaths.first.split('/').last;
} else {
text = '${filePaths.length} ${S.of(context)!.files}';
}
final xfiles = filePaths.map((e) => XFile(e)).toList();
await Share.shareXFiles(xfiles, text: 'ServerBox -> $text');
return filePaths.isNotEmpty;
}
void copy(String text) {
Clipboard.setData(ClipboardData(text: text));
}
Future<String?> pickOneFile() async {
final result = await FilePicker.platform.pickFiles(type: FileType.any);
return result?.files.single.path;
}

View File

@@ -0,0 +1,46 @@
import 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/store/private_key.dart';
import '../../locator.dart';
/// Must put this func out of any Class.
///
/// Because of this function is called by [compute] in [ServerProvider.genClient].
///
/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure
List<SSHKeyPair> loadIndentity(String key) {
return SSHKeyPair.fromPem(key);
}
/// [args] : [key, pwd]
String decyptPem(List<String> args) {
/// skip when the key is not encrypted, or will throw exception
if (!SSHKeyPair.isEncryptedPem(args[0])) return args[0];
final sshKey = SSHKeyPair.fromPem(args[0], args[1]);
return sshKey.first.toPem();
}
Future<SSHClient> genClient(ServerPrivateInfo spi) async {
final socket = await SSHSocket.connect(
spi.ip,
spi.port,
timeout: const Duration(seconds: 5),
);
if (spi.pubKeyId == null) {
return SSHClient(
socket,
username: spi.user,
onPasswordRequest: () => spi.pwd,
);
}
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
return SSHClient(
socket,
username: spi.user,
identities: await compute(loadIndentity, key.privateKey),
);
}

View File

@@ -1,14 +1,13 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/card_dialog.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../view/widget/card_dialog.dart';
import '../persistant_store.dart';
bool isDarkMode(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark;
@@ -71,37 +70,6 @@ void setTransparentNavigationBar(BuildContext context) {
}
}
String tabTitleName(BuildContext context, int i) {
final s = S.of(context);
switch (i) {
case 0:
return s.server;
case 1:
return s.convert;
case 2:
return s.ping;
default:
return '';
}
}
Future<bool> shareFiles(BuildContext context, List<String> filePaths) async {
for (final filePath in filePaths) {
if (!await File(filePath).exists()) {
return false;
}
}
var text = '';
if (filePaths.length == 1) {
text = filePaths.first.split('/').last;
} else {
text = '${filePaths.length} ${S.of(context).files}';
}
final xfiles = filePaths.map((e) => XFile(e)).toList();
await Share.shareXFiles(xfiles, text: 'ServerBox -> $text');
return filePaths.isNotEmpty;
}
Widget buildPopuopMenu(
{required List<PopupMenuEntry> items,
required Function(dynamic) onSelected}) {
@@ -119,3 +87,17 @@ Widget buildPopuopMenu(
),
);
}
String tabTitleName(BuildContext context, int i) {
final s = S.of(context)!;
switch (i) {
case 0:
return s.server;
case 1:
return s.convert;
case 2:
return s.ping;
default:
return '';
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
import '../../../core/utils/ui.dart';
class DynamicColor {
/// 白天模式显示的颜色
Color light;
/// 暗黑模式显示的颜色
Color dark;
DynamicColor(this.light, this.dark);
Color resolve(BuildContext context) => isDarkMode(context) ? dark : light;
}

View File

@@ -1,56 +0,0 @@
import 'package:toolbox/data/model/server/cpu_status.dart';
class Cpu2Status {
List<CpuStatus> _pre;
List<CpuStatus> _now;
String 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;
final used = idleDelta / totalDelta;
return used.isNaN ? 0 : 100 - used * 100;
}
void update(List<CpuStatus> newStatus, String newTemp) {
_pre = _now;
_now = newStatus;
temp = newTemp;
}
int get coresCount => _now.length;
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;
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;
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;
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;
}
double get idle => 100 - usedPercent();
}

View File

@@ -1,7 +1,62 @@
class CpuStatus {
List<OneTimeCpuStatus> _pre;
List<OneTimeCpuStatus> _now;
String temp;
CpuStatus(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;
final used = idleDelta / totalDelta;
return used.isNaN ? 0 : 100 - used * 100;
}
void update(List<OneTimeCpuStatus> newStatus, String newTemp) {
_pre = _now;
_now = newStatus;
temp = newTemp;
}
int get coresCount => _now.length;
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;
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;
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;
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;
}
double get idle => 100 - usedPercent();
}
///
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
///
class CpuStatus {
class OneTimeCpuStatus {
/*
{
"user": 0,
@@ -23,7 +78,7 @@ class CpuStatus {
late int irq;
late int softirq;
CpuStatus(
OneTimeCpuStatus(
this.id,
this.user,
this.sys,
@@ -36,3 +91,51 @@ class CpuStatus {
int get total => user + sys + nice + idle + iowait + irq + softirq;
}
List<OneTimeCpuStatus> parseCPU(String raw) {
final List<OneTimeCpuStatus> 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(OneTimeCpuStatus(
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;
}
final cpuTempReg = RegExp(r'(x86_pkg_temp|cpu_thermal)');
String parseCPUTemp(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';
}

View File

@@ -1,3 +1,5 @@
import '../../res/misc.dart';
class DiskInfo {
/*
{
@@ -10,19 +12,40 @@ class DiskInfo {
}
*/
late String mountPath;
late String mountLocation;
late String path;
late String loc;
late int usedPercent;
late String used;
late String size;
late String avail;
DiskInfo(
this.mountPath,
this.mountLocation,
this.path,
this.loc,
this.usedPercent,
this.used,
this.size,
this.avail,
);
}
List<DiskInfo> parseDisk(String raw) {
final list = <DiskInfo>[];
final items = raw.split('\n');
items.removeAt(0);
for (var item in items) {
if (item.isEmpty) {
continue;
}
final vals = item.split(numReg);
list.add(DiskInfo(
vals[0],
vals[5],
int.parse(vals[4].replaceFirst('%', '')),
vals[2],
vals[1],
vals[3],
));
}
return list;
}

View File

@@ -11,3 +11,70 @@ class Memory {
required this.cache,
required this.avail});
}
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
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,
);
}
class Swap {
final int total;
final int used;
final int free;
final int cached;
Swap({
required this.total,
required this.used,
required this.free,
required this.cached,
});
@override
String toString() {
return 'Swap{total: $total, used: $used, free: $free, cached: $cached}';
}
}
Swap parseSwap(String raw) {
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
final total = int.parse(
items.firstWhere((e) => e?.group(1) == 'SwapTotal:')?.group(2) ?? '1',
);
final free = int.parse(
items.firstWhere((e) => e?.group(1) == 'SwapFree:')?.group(2) ?? '0',
);
final cached = int.parse(
items.firstWhere((e) => e?.group(1) == 'SwapCached:')?.group(2) ?? '0',
);
return Swap(
total: total,
used: total - free,
free: free,
cached: cached,
);
}

View File

@@ -2,9 +2,9 @@ import 'package:toolbox/core/extension/numx.dart';
class NetSpeedPart {
String device;
int bytesIn;
int bytesOut;
int time;
BigInt bytesIn;
BigInt bytesOut;
BigInt time;
NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);
}
@@ -26,7 +26,7 @@ class NetSpeed {
_now = newOne;
}
int get timeDiff => _now[0].time - _old[0].time;
BigInt get timeDiff => _now[0].time - _old[0].time;
String speedIn({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb/s';
@@ -36,6 +36,13 @@ class NetSpeed {
return buildStandardOutput(speedInBytesPerSecond);
}
String totalIn({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb';
final idx = deviceIdx(device);
final totalInBytes = _now[idx].bytesIn;
return totalInBytes.toInt().convertBytes;
}
String speedOut({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb/s';
final idx = deviceIdx(device);
@@ -44,6 +51,13 @@ class NetSpeed {
return buildStandardOutput(speedOutBytesPerSecond);
}
String totalOut({String? device}) {
if (_old[0].device == '' || _now[0].device == '') return '0kb';
final idx = deviceIdx(device);
final totalOutBytes = _now[idx].bytesOut;
return totalOutBytes.toInt().convertBytes;
}
int deviceIdx(String? device) {
if (device != null) {
for (var item in _now) {
@@ -55,6 +69,31 @@ class NetSpeed {
return 0;
}
String buildStandardOutput(double speed) =>
'${speed.convertBytes.toLowerCase()}/s';
String buildStandardOutput(double speed) => '${speed.convertBytes}/s';
}
/// [raw] example:
/// Inter-| Receive | Transmit
/// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901
List<NetSpeedPart> parseNetSpeed(String raw) {
final split = raw.split('\n');
if (split.length < 4) {
return [];
}
final time = BigInt.parse(split[split.length - 1]);
final results = <NetSpeedPart>[];
for (final item in split.sublist(2, split.length - 1)) {
final data = item.trim().split(':');
final device = data.first;
final bytes = data.last.trim().split(' ');
bytes.removeWhere((element) => element == '');
final bytesIn = BigInt.parse(bytes.first);
final bytesOut = BigInt.parse(bytes[8]);
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
}
return results;
}

View File

@@ -2,13 +2,21 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
class ServerInfo {
ServerPrivateInfo info;
class Server {
ServerPrivateInfo spi;
ServerStatus status;
SSHClient? client;
ServerConnectionState connectionState;
ServerState cs;
ServerInfo(this.info, this.status, this.client, this.connectionState);
Server(this.spi, this.status, this.client, this.cs);
}
enum ServerConnectionState { disconnected, connecting, connected, failed }
enum ServerState {
disconnected,
connecting,
connected,
failed;
bool get shouldConnect =>
this == ServerState.disconnected || this == ServerState.failed;
}

View File

@@ -31,15 +31,17 @@ class ServerPrivateInfo {
@HiveField(5)
String? pubKeyId;
String get id => '$user@$ip:$port';
late String id;
ServerPrivateInfo({
required this.name,
required this.ip,
required this.port,
required this.user,
required this.pwd,
this.pubKeyId,
}) : id = '$user@$ip:$port';
ServerPrivateInfo(
{required this.name,
required this.ip,
required this.port,
required this.user,
required this.pwd,
this.pubKeyId});
ServerPrivateInfo.fromJson(Map<String, dynamic> json) {
name = json["name"].toString();
ip = json["ip"].toString();
@@ -47,7 +49,9 @@ class ServerPrivateInfo {
user = json["user"].toString();
pwd = json["authorization"].toString();
pubKeyId = json["pubKeyId"]?.toString();
id = '$user@$ip:$port';
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["name"] = name;

View File

@@ -1,4 +1,4 @@
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/disk_info.dart';
import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
@@ -30,8 +30,9 @@ class ServerStatus {
}
*/
Cpu2Status cpu2Status;
Memory memory;
CpuStatus cpu;
Memory mem;
Swap swap;
String sysVer;
String uptime;
List<DiskInfo> disk;
@@ -39,7 +40,15 @@ class ServerStatus {
NetSpeed netSpeed;
String? failedInfo;
ServerStatus(this.cpu2Status, this.memory, this.sysVer, this.uptime,
this.disk, this.tcp, this.netSpeed,
{this.failedInfo});
ServerStatus({
required this.cpu,
required this.mem,
required this.sysVer,
required this.uptime,
required this.disk,
required this.tcp,
required this.netSpeed,
required this.swap,
this.failedInfo,
});
}

View File

@@ -1,3 +1,6 @@
import '../../../core/extension/stringx.dart';
import '../../res/misc.dart';
///
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
///
@@ -39,3 +42,14 @@ class TcpStatus {
return data;
}
}
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;
}

View File

@@ -0,0 +1,16 @@
import '../server/server_private_info.dart';
class DownloadItem {
DownloadItem(this.spi, this.remotePath, this.localPath);
final ServerPrivateInfo spi;
final String remotePath;
final String localPath;
}
class DownloadItemEvent {
DownloadItemEvent(this.item, this.privateKey);
final DownloadItem item;
final String? privateKey;
}

View File

@@ -1,5 +1,7 @@
import 'package:toolbox/data/model/sftp/download_worker.dart';
import 'download_item.dart';
class SftpDownloadStatus {
final int id;
final DownloadItem item;

View File

@@ -4,16 +4,9 @@ import 'dart:isolate';
import 'package:dartssh2/dartssh2.dart';
import 'package:easy_isolate/easy_isolate.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
class DownloadItem {
DownloadItem(this.spi, this.remotePath, this.localPath);
final ServerPrivateInfo spi;
final String remotePath;
final String localPath;
}
import 'download_item.dart';
import 'download_status.dart';
class SftpDownloadWorker {
SftpDownloadWorker(
@@ -101,10 +94,3 @@ class SftpDownloadWorker {
}
}
}
class DownloadItemEvent {
DownloadItemEvent(this.item, this.privateKey);
final DownloadItem item;
final String? privateKey;
}

View File

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:xterm/ui.dart';
import '../../res/terminal_color.dart';
class TerminalUITheme {
final Color cursor;
final Color selection;
final Color foreground;
final Color background;
final Color searchHitBackground;
final Color searchHitBackgroundCurrent;
final Color searchHitForeground;
const TerminalUITheme({
required this.cursor,
required this.selection,
required this.foreground,
required this.background,
required this.searchHitBackground,
required this.searchHitBackgroundCurrent,
required this.searchHitForeground,
});
TerminalTheme toTerminalTheme(TerminalColors termColor) {
return TerminalTheme(
cursor: cursor,
selection: selection,
foreground: foreground,
background: background,
black: termColor.black,
red: termColor.red,
green: termColor.green,
yellow: termColor.yellow,
blue: termColor.blue,
magenta: termColor.magenta,
cyan: termColor.cyan,
white: termColor.white,
brightBlack: termColor.brightBlack,
brightRed: termColor.brightRed,
brightGreen: termColor.brightGreen,
brightYellow: termColor.brightYellow,
brightBlue: termColor.brightBlue,
brightMagenta: termColor.brightMagenta,
brightCyan: termColor.brightCyan,
brightWhite: termColor.brightWhite,
searchHitBackground: searchHitBackground,
searchHitBackgroundCurrent: searchHitBackgroundCurrent,
searchHitForeground: searchHitForeground,
);
}
}
abstract class TerminalColors {
final TerminalColorsPlatform platform;
final Color black = Colors.black;
final Color red;
final Color green;
final Color yellow;
final Color blue;
// 品红
final Color magenta;
// 青
final Color cyan;
final Color white;
/// Also called grey
final Color brightBlack;
final Color brightRed;
final Color brightGreen;
final Color brightYellow;
final Color brightBlue;
final Color brightMagenta;
final Color brightCyan;
final Color brightWhite;
TerminalColors(
this.platform,
this.red,
this.green,
this.yellow,
this.blue,
this.magenta,
this.cyan,
this.white,
this.brightBlack,
this.brightRed,
this.brightGreen,
this.brightYellow,
this.brightBlue,
this.brightMagenta,
this.brightCyan, {
this.brightWhite = Colors.white,
});
}
enum TerminalColorsPlatform {
macOS,
vga,
cmd,
putty,
xterm,
ubuntu,
;
String get name {
switch (this) {
case TerminalColorsPlatform.vga:
return 'VGA';
case TerminalColorsPlatform.cmd:
return 'CMD';
case TerminalColorsPlatform.macOS:
return 'macOS';
case TerminalColorsPlatform.putty:
return 'PuTTY';
case TerminalColorsPlatform.xterm:
return 'XTerm';
case TerminalColorsPlatform.ubuntu:
return 'Ubuntu';
default:
return 'Unknown';
}
}
TerminalColors get colors {
switch (this) {
case TerminalColorsPlatform.vga:
return VGATerminalColor();
case TerminalColorsPlatform.cmd:
return CMDTerminalColor();
case TerminalColorsPlatform.macOS:
return MacOSTerminalColor();
case TerminalColorsPlatform.putty:
return PuttyTerminalColor();
case TerminalColorsPlatform.xterm:
return XTermTerminalColor();
case TerminalColorsPlatform.ubuntu:
return UbuntuTerminalColor();
default:
return MacOSTerminalColor();
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import 'package:xterm/core.dart';
class VirtualKey {
final String text;
final bool toggleable;
final TerminalKey? key;
final IconData? icon;
final VirtualKeyFunc? func;
VirtualKey(this.text,
{this.key, this.toggleable = false, this.icon, this.func});
}
enum VirtualKeyFunc { toggleIME, backspace, copy, paste }

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../../data/res/misc.dart';
/// format: [NAME][LEVEL]: MESSAGE
final _headReg = RegExp(r'(\[[A-Za-z]+\])(\[[A-Z]+\]): (.*)');
const _level2Color = {
@@ -14,7 +16,7 @@ class DebugProvider extends ChangeNotifier {
final match = _headReg.allMatches(text);
if (match.isNotEmpty) {
addWidget(Text.rich(TextSpan(
_addWidget(Text.rich(TextSpan(
children: [
TextSpan(
text: match.first.group(1),
@@ -30,31 +32,11 @@ class DebugProvider extends ChangeNotifier {
],
)));
} else {
_addText(text);
_addWidget(Text(text));
}
notifyListeners();
}
void _addText(String text) {
_addWidget(Text(text));
}
void addError(Object error) {
_addError(error);
notifyListeners();
}
void _addError(Object error) {
_addMultiline(error, Colors.red);
}
void addMultiline(Object data, [Color color = Colors.blue]) {
_addMultiline(data, color);
notifyListeners();
}
void _addMultiline(Object data, [Color color = Colors.blue]) {
final widget = Text(
'$data',
style: TextStyle(
@@ -67,14 +49,13 @@ class DebugProvider extends ChangeNotifier {
));
}
void addWidget(Widget widget) {
_addWidget(widget);
notifyListeners();
}
void _addWidget(Widget widget) {
widgets.add(widget);
widgets.add(const SizedBox(height: 13));
if (widgets.length > maxDebugLogLines) {
widgets.removeRange(0, widgets.length - maxDebugLogLines);
}
notifyListeners();
}
void clear() {

View File

@@ -53,6 +53,7 @@ class DockerProvider extends BusyProvider {
}
Future<void> refresh() async {
if (isBusy) return;
final verRaw = await client!.run('docker version'.withLangExport).string;
if (verRaw.contains(_dockerNotFound)) {
error = DockerErr(type: DockerErrType.notInstalled);

View File

@@ -1,111 +1,58 @@
import 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/server/cpu_2_status.dart';
import 'package:toolbox/data/model/server/cpu_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/disk_info.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/model/server/tcp_status.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/locator.dart';
/// Must put this func out of any Class.
/// Because of this function is called by [compute] in [ServerProvider.genClient].
/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure
List<SSHKeyPair> loadIndentity(String key) {
return SSHKeyPair.fromPem(key);
}
const seperator = 'A====A';
const shellCmd = "export LANG=en_US.utf-8 \necho '$seperator' \n"
"cat /proc/net/dev && date +%s \necho $seperator \n "
"cat /etc/os-release | grep PRETTY_NAME \necho $seperator \n"
"cat /proc/stat | grep cpu \necho $seperator \n"
"uptime \necho $seperator \n"
"cat /proc/net/snmp \necho $seperator \n"
"df -h \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';
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');
import '../../core/extension/uint8list.dart';
import '../../core/provider_base.dart';
import '../../core/utils/server.dart';
import '../../locator.dart';
import '../model/server/cpu_status.dart';
import '../model/server/disk_info.dart';
import '../model/server/memory.dart';
import '../model/server/net_speed.dart';
import '../model/server/server.dart';
import '../model/server/server_private_info.dart';
import '../model/server/snippet.dart';
import '../model/server/tcp_status.dart';
import '../res/server_cmd.dart';
import '../res/status.dart';
import '../store/server.dart';
import '../store/setting.dart';
class ServerProvider extends BusyProvider {
List<ServerInfo> _servers = [];
List<ServerInfo> get servers => _servers;
List<Server> _servers = [];
List<Server> get servers => _servers;
final _TryLimiter _limiter = _TryLimiter();
Timer? _timer;
final logger = Logger('SERVER');
Memory get emptyMemory =>
Memory(total: 1, used: 0, free: 1, cache: 0, avail: 1);
NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0);
NetSpeed get emptyNetSpeed =>
NetSpeed([emptyNetSpeedPart], [emptyNetSpeedPart]);
CpuStatus get emptyCpuStatus => CpuStatus('cpu', 0, 0, 0, 0, 0, 0, 0);
Cpu2Status get emptyCpu2Status =>
Cpu2Status([emptyCpuStatus], [emptyCpuStatus], '');
ServerStatus get emptyStatus => ServerStatus(
emptyCpu2Status,
emptyMemory,
'Loading...',
'',
[DiskInfo('/', '/', 0, '0', '0', '0')],
TcpStatus(0, 0, 0, 0),
emptyNetSpeed);
final _logger = Logger('SERVER');
Future<void> loadLocalData() async {
setBusyState(true);
final infos = locator<ServerStore>().fetch();
_servers = List.generate(infos.length, (index) => genInfo(infos[index]));
_servers = List.generate(infos.length, (index) => genServer(infos[index]));
setBusyState(false);
notifyListeners();
}
ServerInfo genInfo(ServerPrivateInfo spi) {
return ServerInfo(
spi, emptyStatus, null, ServerConnectionState.disconnected);
Server genServer(ServerPrivateInfo spi) {
return Server(spi, initStatus, null, ServerState.disconnected);
}
Future<SSHClient> genClient(ServerPrivateInfo spi) async {
final socket = await SSHSocket.connect(spi.ip, spi.port);
if (spi.pubKeyId == null) {
return SSHClient(socket,
username: spi.user, onPasswordRequest: () => spi.pwd);
}
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
return SSHClient(socket,
username: spi.user,
identities: await compute(loadIndentity, key.privateKey));
}
Future<void> refreshData({ServerPrivateInfo? spi}) async {
Future<void> refreshData(
{ServerPrivateInfo? spi, bool onlyFailed = false}) async {
if (spi != null) {
_getData(spi);
await _getData(spi);
return;
}
await Future.wait(_servers.map((s) async {
await _getData(s.info);
if (onlyFailed) {
if (s.cs != ServerState.failed) return;
_limiter.resetTryTimes(s.spi.id);
}
await _getData(s.spi);
}));
}
@@ -128,8 +75,10 @@ class ServerProvider extends BusyProvider {
void setDisconnected() {
for (var i = 0; i < _servers.length; i++) {
_servers[i].connectionState = ServerConnectionState.disconnected;
_servers[i].cs = ServerState.disconnected;
}
_limiter.clear();
notifyListeners();
}
void closeServer({ServerPrivateInfo? spi}) {
@@ -138,172 +87,173 @@ class ServerProvider extends BusyProvider {
_servers[i].client?.close();
_servers[i].client = null;
}
notifyListeners();
return;
}
final idx = _servers.indexWhere((e) => e.info == spi);
if (idx < 0) {
throw RangeError.index(idx, _servers);
}
final idx = getServerIdx(spi.id);
_servers[idx].client?.close();
_servers[idx].client = null;
notifyListeners();
}
void addServer(ServerPrivateInfo spi) {
_servers.add(genInfo(spi));
locator<ServerStore>().put(spi);
_servers.add(genServer(spi));
notifyListeners();
locator<ServerStore>().put(spi);
refreshData(spi: spi);
}
void delServer(ServerPrivateInfo info) {
final idx = _servers.indexWhere((s) => s.info == info);
if (idx == -1) return;
void delServer(String id) {
final idx = getServerIdx(id);
_servers[idx].client?.close();
_servers.removeAt(idx);
notifyListeners();
locator<ServerStore>().delete(info);
locator<ServerStore>().delete(id);
}
Future<void> updateServer(
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
final idx = _servers.indexWhere((e) => e.info == old);
final idx = _servers.indexWhere((e) => e.spi.id == old.id);
if (idx < 0) {
throw RangeError.index(idx, _servers);
}
_servers[idx].info = newSpi;
_servers[idx].spi = newSpi;
locator<ServerStore>().update(old, newSpi);
_servers[idx].client = await genClient(newSpi);
notifyListeners();
refreshData(spi: newSpi);
}
int getServerIdx(String id) {
final idx = _servers.indexWhere((s) => s.spi.id == id);
if (idx < 0) {
throw Exception('Server not found: $id');
}
return idx;
}
Server getServer(String id) => _servers[getServerIdx(id)];
Future<void> _getData(ServerPrivateInfo spi) async {
final s = _servers.firstWhere((element) => element.info == spi);
final state = s.connectionState;
if (state == ServerConnectionState.failed ||
state == ServerConnectionState.disconnected) {
s.connectionState = ServerConnectionState.connecting;
final sid = spi.id;
final s = getServer(sid);
final state = s.cs;
if (state.shouldConnect) {
if (!_limiter.shouldTry(sid)) {
s.cs = ServerState.failed;
notifyListeners();
return;
}
s.cs = ServerState.connecting;
notifyListeners();
final time1 = DateTime.now();
try {
// try to connect
final time1 = DateTime.now();
s.client = await genClient(spi);
final time2 = DateTime.now();
logger.info(
'Connected to [${spi.name}] in [${time2.difference(time1).toString()}].');
s.connectionState = ServerConnectionState.connected;
final spentTime = time2.difference(time1).inMilliseconds;
_logger.info('Connected to [$sid] in $spentTime ms.');
// after connected
s.cs = ServerState.connected;
final writeResult = await s.client!
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath")
.string;
// if write failed
if (writeResult.isNotEmpty) {
throw Exception(writeResult);
}
_limiter.resetTryTimes(sid);
} catch (e) {
s.connectionState = ServerConnectionState.failed;
s.status.failedInfo = '$e ## ';
logger.warning(e);
s.cs = ServerState.failed;
s.status.failedInfo = '$e';
_logger.warning(e);
} finally {
notifyListeners();
}
}
// if client is null, return
if (s.client == null) return;
// run script to get server status
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;
s.cs = ServerState.failed;
if (s.status.failedInfo == null || s.status.failedInfo!.isEmpty) {
s.status.failedInfo = 'No data received';
s.status.failedInfo = 'Seperate segments failed, raw:\n$raw';
}
notifyListeners();
return;
}
// remove first empty segment
segments.removeAt(0);
try {
_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]);
_getCPU(sid, segments[2], segments[7], segments[8]);
_getMem(sid, segments[6]);
_getSysVer(sid, segments[1]);
_getUpTime(sid, segments[3]);
_getDisk(sid, segments[5]);
_getTcp(sid, segments[4]);
_getNetSpeed(sid, segments[0]);
} catch (e) {
s.connectionState = ServerConnectionState.failed;
s.cs = ServerState.failed;
s.status.failedInfo = e.toString();
logger.warning(e);
_logger.warning(e);
rethrow;
} finally {
notifyListeners();
}
}
/// [raw] example:
/// Inter-| Receive | Transmit
/// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901
Future<void> _getNetSpeed(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
info.status.netSpeed.update(await compute(_parseNetSpeed, raw));
Future<void> _getNetSpeed(String id, String raw) async {
final net = await compute(parseNetSpeed, raw);
getServer(id).status.netSpeed.update(net);
}
void _getSysVer(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
void _getSysVer(String id, String raw) {
final s = raw.split('=');
if (s.length == 2) {
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
final ver = s[1].replaceAll('"', '').replaceFirst('\n', '');
getServer(id).status.sysVer = ver;
}
}
Future<void> _getCPU(ServerPrivateInfo spi, String raw, String tempType,
String tempValue) async {
final info = _servers.firstWhere((e) => e.info == spi);
final cpus = await compute(_parseCPU, raw);
Future<void> _getCPU(
String id, String raw, String tempType, String tempValue) async {
final cpus = await compute(parseCPU, raw);
final temp = await compute(parseCPUTemp, [tempType, tempValue]);
if (cpus.isNotEmpty) {
info.status.cpu2Status
.update(cpus, await compute(_getCPUTemp, [tempType, tempValue]));
getServer(id).status.cpu.update(cpus, temp);
}
}
void _getUpTime(ServerPrivateInfo spi, String raw) {
_servers.firstWhere((e) => e.info == spi).status.uptime =
raw.split('up ')[1].split(', ')[0];
void _getUpTime(String id, String raw) {
getServer(id).status.uptime = raw.split('up ')[1].split(', ')[0];
}
Future<void> _getTcp(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
final status = await compute(_parseTcp, raw);
Future<void> _getTcp(String id, String raw) async {
final status = await compute(parseTcp, raw);
if (status != null) {
info.status.tcp = status;
getServer(id).status.tcp = status;
}
}
void _getDisk(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final list = <DiskInfo>[];
final items = raw.split('\n');
for (var item in items) {
if (items.indexOf(item) == 0 || item.isEmpty) {
continue;
}
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;
Future<void> _getDisk(String id, String raw) async {
getServer(id).status.disk = await compute(parseDisk, raw);
}
Future<void> _getMem(ServerPrivateInfo spi, String raw) async {
final info = _servers.firstWhere((e) => e.info == spi);
final mem = await compute(_parseMem, raw);
info.status.memory = mem;
Future<void> _getMem(String id, String raw) async {
final s = getServer(id);
s.status.mem = await compute(parseMem, raw);
s.status.swap = await compute(parseSwap, raw);
}
Future<String?> runSnippet(String id, Snippet snippet) async {
final client =
_servers.firstWhere((element) => element.info.id == id).client;
final client = getServer(id).client;
if (client == null) {
return null;
}
@@ -311,95 +261,27 @@ class ServerProvider extends BusyProvider {
}
}
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);
}
class _TryLimiter {
final Map<String, int> _triedTimes = {};
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;
bool shouldTry(String id) {
final maxCount = locator<SettingStore>().maxRetryCount.fetch()!;
if (maxCount <= 0) {
return true;
}
idx++;
final times = _triedTimes[id] ?? 0;
if (times >= maxCount) {
return false;
}
_triedTimes[id] = times + 1;
return true;
}
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));
void resetTryTimes(String id) {
_triedTimes[id] = 0;
}
void clear() {
_triedTimes.clear();
}
return results;
}

View File

@@ -1,6 +1,7 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
import 'package:toolbox/data/model/sftp/download_worker.dart';
import '../model/sftp/download_item.dart';
import '../model/sftp/download_status.dart';
class SftpDownloadProvider extends ProviderBase {
final List<SftpDownloadStatus> _status = [];

View File

@@ -0,0 +1,29 @@
import 'package:flutter/widgets.dart';
import 'package:xterm/core.dart';
class VirtualKeyboard extends TerminalInputHandler with ChangeNotifier {
VirtualKeyboard();
bool ctrl = false;
bool alt = false;
void reset(TerminalKeyboardEvent e) {
if (e.ctrl) {
ctrl = false;
}
if (e.alt) {
alt = false;
}
notifyListeners();
}
@override
String? call(TerminalKeyboardEvent event) {
final e = event.copyWith(
ctrl: event.ctrl || ctrl,
alt: event.alt || alt,
);
reset(e);
return defaultInputHandler.call(e);
}
}

View File

@@ -2,9 +2,9 @@
class BuildData {
static const String name = "ServerBox";
static const int build = 187;
static const int build = 227;
static const String engine =
"Flutter 3.7.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b06b8b2710 (4 days ago) • 2023-01-23 16:55:55 -0800\nEngine • revision b24591ed32\nTools • Dart 2.19.0 • DevTools 2.20.1\n";
static const String buildAt = "2023-01-28 13:54:17.985459";
"Flutter 3.7.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 9944297138 (2 weeks ago) • 2023-02-08 15:46:04 -0800\nEngine • revision 248290d6d5\nTools • Dart 2.19.2 • DevTools 2.20.1\n";
static const String buildAt = "2023-02-25 18:23:00.718232";
static const int modifications = 13;
}

View File

@@ -1,21 +1,11 @@
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart';
Color get primaryColor => Color(locator<SettingStore>().primaryColor.fetch()!);
import '../model/app/dynamic_color.dart';
class DynamicColor {
/// 白天模式显示的颜色
Color light;
Color primaryColor = Color(locator<SettingStore>().primaryColor.fetch()!);
/// 暗黑模式显示的颜色
Color dark;
DynamicColor(this.light, this.dark);
resolve(BuildContext context) => isDarkMode(context) ? dark : light;
}
final mainColor = DynamicColor(Colors.black87, Colors.white70);
final contentColor = DynamicColor(Colors.black87, Colors.white70);
final bgColor = DynamicColor(Colors.white, Colors.black);
final progressColor = DynamicColor(Colors.grey.shade100, Colors.white10);

View File

@@ -1,27 +1,7 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class DropdownBtnItem {
final String text;
final IconData icon;
const DropdownBtnItem({
required this.text,
required this.icon,
});
Widget get build => Row(
children: [
Icon(icon, color: primaryColor),
const SizedBox(
width: 10,
),
Text(
text,
),
],
);
}
import '../../view/widget/dropdown_menu.dart';
class ServerTabMenuItems {
static const List<DropdownBtnItem> firstItems = [sftp, snippet, pkg, docker];
@@ -42,3 +22,22 @@ class DockerMenuItems {
static const start = DropdownBtnItem(text: 'Start', icon: Icons.play_arrow);
static const stop = DropdownBtnItem(text: 'Stop', icon: Icons.stop);
}
String getDropdownBtnText(S s, String text) {
switch (text) {
case 'Snippet':
return s.snippet;
case 'Pkg':
return s.pkg;
case 'Remove':
return s.delete;
case 'Start':
return s.start;
case 'Stop':
return s.stop;
case 'Edit':
return s.edit;
default:
return text;
}
}

7
lib/data/res/misc.dart Normal file
View File

@@ -0,0 +1,7 @@
final numReg = RegExp(r'\s{1,}');
/// Private Key max allowed size is 20kb
const privateKeyMaxSize = 20 * 1024;
/// Max debug log lines
const maxDebugLogLines = 100;

View File

@@ -1,3 +1,3 @@
import 'package:flutter/material.dart';
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17);
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17, vertical: 13);

View File

@@ -0,0 +1,12 @@
const seperator = 'A====A';
const shellCmd = "export LANG=en_US.utf-8 \necho '$seperator' \n"
"cat /proc/net/dev && date +%s \necho $seperator \n"
"cat /etc/os-release | grep PRETTY_NAME \necho $seperator \n"
"cat /proc/stat | grep cpu \necho $seperator \n"
"uptime \necho $seperator \n"
"cat /proc/net/snmp \necho $seperator \n"
"df -h \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';

View File

@@ -0,0 +1,5 @@
import 'package:flutter/widgets.dart';
const height13 = SizedBox(height: 13);
const width13 = SizedBox(width: 13);
const width7 = SizedBox(width: 7);

55
lib/data/res/status.dart Normal file
View File

@@ -0,0 +1,55 @@
import '../model/server/cpu_status.dart';
import '../model/server/disk_info.dart';
import '../model/server/memory.dart';
import '../model/server/net_speed.dart';
import '../model/server/server_status.dart';
import '../model/server/tcp_status.dart';
get _initMemory => Memory(
total: 1,
used: 0,
free: 1,
cache: 0,
avail: 1,
);
get _initOneTimeCpuStatus => OneTimeCpuStatus(
'cpu',
0,
0,
0,
0,
0,
0,
0,
);
get initCpuStatus => CpuStatus(
[_initOneTimeCpuStatus],
[_initOneTimeCpuStatus],
'',
);
get _initNetSpeedPart => NetSpeedPart(
'',
BigInt.zero,
BigInt.zero,
BigInt.zero,
);
get initNetSpeed => NetSpeed(
[_initNetSpeedPart],
[_initNetSpeedPart],
);
get _initSwap => Swap(
total: 1,
used: 0,
free: 1,
cached: 0,
);
get initStatus => ServerStatus(
cpu: initCpuStatus,
mem: _initMemory,
sysVer: 'Loading...',
uptime: '',
disk: [DiskInfo('/', '/', 0, '0', '0', '0')],
tcp: TcpStatus(0, 0, 0, 0),
netSpeed: initNetSpeed,
swap: _initSwap,
);

View File

@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/model/ssh/terminal_color.dart';
class VGATerminalColor extends TerminalColors {
VGATerminalColor()
: super(
TerminalColorsPlatform.vga,
const Color.fromARGB(255, 170, 0, 0),
const Color.fromARGB(255, 0, 170, 0),
const Color.fromARGB(255, 170, 85, 0),
const Color.fromARGB(255, 0, 0, 170),
const Color.fromARGB(255, 170, 0, 170),
const Color.fromARGB(255, 0, 170, 170),
const Color.fromARGB(255, 170, 170, 170),
const Color.fromARGB(255, 85, 85, 85),
const Color.fromARGB(255, 255, 85, 85),
const Color.fromARGB(255, 85, 255, 85),
const Color.fromARGB(255, 255, 255, 85),
const Color.fromARGB(255, 85, 85, 255),
const Color.fromARGB(255, 255, 85, 255),
const Color.fromARGB(255, 85, 255, 255),
);
}
class CMDTerminalColor extends TerminalColors {
CMDTerminalColor()
: super(
TerminalColorsPlatform.cmd,
const Color.fromARGB(255, 128, 0, 0),
const Color.fromARGB(255, 0, 128, 0),
const Color.fromARGB(255, 128, 128, 0),
const Color.fromARGB(255, 0, 0, 128),
const Color.fromARGB(255, 128, 0, 128),
const Color.fromARGB(255, 0, 128, 128),
const Color.fromARGB(255, 192, 192, 192),
const Color.fromARGB(255, 128, 128, 128),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 255, 0),
const Color.fromARGB(255, 0, 0, 255),
const Color.fromARGB(255, 255, 0, 255),
const Color.fromARGB(255, 0, 255, 255),
);
}
class MacOSTerminalColor extends TerminalColors {
MacOSTerminalColor()
: super(
TerminalColorsPlatform.macOS,
const Color.fromARGB(255, 194, 54, 33),
const Color.fromARGB(255, 37, 188, 36),
const Color.fromARGB(255, 173, 173, 39),
const Color.fromARGB(255, 73, 46, 225),
const Color.fromARGB(255, 211, 56, 211),
const Color.fromARGB(255, 51, 187, 200),
const Color.fromARGB(255, 203, 204, 205),
const Color.fromARGB(255, 129, 131, 131),
const Color.fromARGB(255, 252, 57, 31),
const Color.fromARGB(255, 49, 231, 34),
const Color.fromARGB(255, 234, 236, 35),
const Color.fromARGB(255, 88, 51, 255),
const Color.fromARGB(255, 249, 53, 248),
const Color.fromARGB(255, 20, 240, 240),
brightWhite: const Color.fromARGB(255, 233, 235, 235));
}
class PuttyTerminalColor extends TerminalColors {
PuttyTerminalColor()
: super(
TerminalColorsPlatform.putty,
const Color.fromARGB(255, 187, 0, 0),
const Color.fromARGB(255, 0, 187, 0),
const Color.fromARGB(255, 187, 187, 0),
const Color.fromARGB(255, 0, 0, 187),
const Color.fromARGB(255, 187, 0, 187),
const Color.fromARGB(255, 0, 187, 187),
const Color.fromARGB(255, 187, 187, 187),
const Color.fromARGB(255, 85, 85, 85),
const Color.fromARGB(255, 255, 85, 85),
const Color.fromARGB(255, 85, 255, 85),
const Color.fromARGB(255, 255, 255, 85),
const Color.fromARGB(255, 85, 85, 255),
const Color.fromARGB(255, 255, 85, 255),
const Color.fromARGB(255, 85, 255, 255),
);
}
class XTermTerminalColor extends TerminalColors {
XTermTerminalColor()
: super(
TerminalColorsPlatform.xterm,
const Color.fromARGB(255, 205, 0, 0),
const Color.fromARGB(255, 0, 205, 0),
const Color.fromARGB(255, 205, 205, 0),
const Color.fromARGB(255, 0, 0, 238),
const Color.fromARGB(255, 205, 0, 205),
const Color.fromARGB(255, 0, 205, 205),
const Color.fromARGB(255, 229, 229, 229),
const Color.fromARGB(255, 127, 127, 127),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 255, 0),
const Color.fromARGB(255, 92, 92, 255),
const Color.fromARGB(255, 255, 0, 255),
const Color.fromARGB(255, 0, 255, 255),
);
}
class UbuntuTerminalColor extends TerminalColors {
UbuntuTerminalColor()
: super(
TerminalColorsPlatform.ubuntu,
const Color.fromARGB(255, 222, 56, 43),
const Color.fromARGB(255, 57, 181, 74),
const Color.fromARGB(255, 255, 199, 6),
const Color.fromARGB(255, 0, 111, 184),
const Color.fromARGB(255, 118, 38, 113),
const Color.fromARGB(255, 44, 181, 233),
const Color.fromARGB(255, 204, 204, 204),
const Color.fromARGB(255, 128, 128, 128),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 255, 0),
const Color.fromARGB(255, 0, 0, 255),
const Color.fromARGB(255, 255, 0, 255),
const Color.fromARGB(255, 0, 255, 255),
);
}

View File

@@ -1,53 +1,22 @@
import 'package:flutter/material.dart';
import 'package:xterm/ui.dart';
const termDarkTheme = TerminalTheme(
import '../model/ssh/terminal_color.dart';
const termDarkTheme = TerminalUITheme(
cursor: Color(0XAAAEAFAD),
selection: Color(0XAAAEAFAD),
foreground: Color(0XFFCCCCCC),
background: Colors.black,
black: Color(0XFF000000),
red: Color(0XFFCD3131),
green: Color(0XFF0DBC79),
yellow: Color(0XFFE5E510),
blue: Color(0XFF2472C8),
magenta: Color(0XFFBC3FBC),
cyan: Color(0XFF11A8CD),
white: Color(0XFFE5E5E5),
brightBlack: Color(0XFF666666),
brightRed: Color(0XFFF14C4C),
brightGreen: Color(0XFF23D18B),
brightYellow: Color(0XFFF5F543),
brightBlue: Color(0XFF3B8EEA),
brightMagenta: Color(0XFFD670D6),
brightCyan: Color(0XFF29B8DB),
brightWhite: Color(0XFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),
);
const termLightTheme = TerminalTheme(
const termLightTheme = TerminalUITheme(
cursor: Color(0XFFAEAFAD),
selection: Color(0XFFAEAFAD),
selection: Color.fromARGB(102, 174, 175, 173),
foreground: Color(0XFF000000),
background: Color(0XFFFFFFFF),
black: Color(0XFF000000),
red: Color(0XFFCD3131),
green: Color(0XFF0DBC79),
yellow: Color(0XFFE5E510),
blue: Color(0XFF2472C8),
magenta: Color(0XFFBC3FBC),
cyan: Color(0XFF11A8CD),
white: Color(0XFFE5E5E5),
brightBlack: Color(0XFF666666),
brightRed: Color(0XFFF14C4C),
brightGreen: Color(0XFF23D18B),
brightYellow: Color(0XFFF5F543),
brightBlue: Color(0XFF3B8EEA),
brightMagenta: Color(0XFFD670D6),
brightCyan: Color(0XFF29B8DB),
brightWhite: Color(0XFFFFFFFF),
searchHitBackground: Color(0XFFFFFF2B),
searchHitBackgroundCurrent: Color(0XFF31FF26),
searchHitForeground: Color(0XFF000000),

View File

@@ -2,6 +2,13 @@ const backendUrl = 'https://res.lolli.tech';
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 = '$myGithub/flutter_server_box/issues';
// Thanks
const thanksMap = {
'RainSunMe': 'https://github.com/RainSunMe',
'fecture': 'https://github.com/fecture',
'Tao173': 'https://github.com/Tao173',
'QingAnLe': 'https://github.com/QingAnLe',
'wxdjs': 'https://github.com/wxdjs',
};

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:xterm/core.dart';
import '../model/ssh/virtual_key.dart';
var virtualKeys = [
VirtualKey('Esc', key: TerminalKey.escape),
VirtualKey('Alt', key: TerminalKey.alt, toggleable: true),
VirtualKey('Home', key: TerminalKey.home),
VirtualKey('Up', key: TerminalKey.arrowUp, icon: Icons.arrow_upward),
VirtualKey('End', key: TerminalKey.end),
// VirtualKey(
// 'Del',
// key: TerminalKey.delete,
// ),
VirtualKey('Paste', func: VirtualKeyFunc.paste, icon: Icons.paste),
VirtualKey('Tab', key: TerminalKey.tab),
VirtualKey('Ctrl', key: TerminalKey.control, toggleable: true),
VirtualKey('Left', key: TerminalKey.arrowLeft, icon: Icons.arrow_back),
VirtualKey('Down', key: TerminalKey.arrowDown, icon: Icons.arrow_downward),
VirtualKey('Right', key: TerminalKey.arrowRight, icon: Icons.arrow_forward),
VirtualKey(
'IME',
func: VirtualKeyFunc.toggleIME,
icon: Icons.keyboard_hide,
),
];

View File

@@ -18,14 +18,15 @@ class ServerStore extends PersistentStore {
return ss;
}
void delete(ServerPrivateInfo s) {
box.delete(s.id);
void delete(String id) {
box.delete(id);
}
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
if (!have(old)) {
throw Exception('Old ServerPrivateInfo not found');
}
delete(old.id);
put(newInfo);
}

View File

@@ -6,7 +6,7 @@ class SettingStore extends PersistentStore {
property('primaryColor', defaultValue: Colors.deepPurpleAccent.value);
StoreProperty<int> get serverStatusUpdateInterval =>
property('serverStatusUpdateInterval', defaultValue: 5);
property('serverStatusUpdateInterval', defaultValue: 3);
/// Lanch page idx
StoreProperty<int> get launchPage => property('launchPage', defaultValue: 0);
@@ -18,4 +18,19 @@ class SettingStore extends PersistentStore {
/// Show logo on server detail page
StoreProperty<bool> get showDistLogo =>
property('showDistLogo', defaultValue: true);
/// First time to use SSH term
StoreProperty<bool> get firstTimeUseSshTerm =>
property('firstTimeUseSshTerm', defaultValue: true);
StoreProperty<int> get termColorIdx =>
property('termColorIdx', defaultValue: 0);
/// Max retry count when connect to server
StoreProperty<int> get maxRetryCount =>
property('maxRetryCount', defaultValue: 7);
/// Night mode: 0 -> auto, 1 -> light, 2 -> dark
StoreProperty<int> get nightMode =>
property('nightMode', defaultValue: ThemeMode.system.index);
}

View File

@@ -1,67 +0,0 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en;
import 'messages_zh.dart' as messages_zh;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new SynchronousFuture(null),
'zh': () => new SynchronousFuture(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
case 'zh':
return messages_zh.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new SynchronousFuture(false);
}
var lib = _deferredLibraries[availableLocale];
lib == null ? new SynchronousFuture(false) : lib();
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new SynchronousFuture(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale =
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}

View File

@@ -1,263 +0,0 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a en locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
static String m0(fileName) => "Download [${fileName}] to local?";
static String m1(count) => "${count} images";
static String m2(runningCount, stoppedCount) =>
"${runningCount} running, ${stoppedCount} container stopped.";
static String m3(count) => "${count} container running.";
static String m4(percent, size) => "${percent}% of ${size}";
static String m5(count) => "Found ${count} update";
static String m6(code) => "request failed, status code: ${code}";
static String m7(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 m9(url) => "Please report bugs on ${url}";
static String m10(date) => "Are you sure to restore from ${date} ?";
static String m11(time) => "Spent time: ${time}";
static String m12(url) =>
"This function is now in the experimental stage. \nPlease report bugs on ${url} or join our development.";
static String m13(name) => "Are you sure to delete [${name}]?";
static String m14(server) => "Are you sure to delete server [${server}]?";
static String m15(build) => "Found: v1.0.${build}, click to update";
static String m16(build) => "Current: v1.0.${build}";
static String m17(build) => "Current: v1.0.${build}, is up to date";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": MessageLookupByLibrary.simpleMessage(
"\nAll rights reserved.\n\nThanks to the following people who participated in the test."),
"addAServer": MessageLookupByLibrary.simpleMessage("add a server"),
"addOne": MessageLookupByLibrary.simpleMessage("Add one"),
"addPrivateKey":
MessageLookupByLibrary.simpleMessage("Add private key"),
"alreadyLastDir":
MessageLookupByLibrary.simpleMessage("Already in last directory."),
"appPrimaryColor":
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":
MessageLookupByLibrary.simpleMessage("Choose destination"),
"choosePrivateKey":
MessageLookupByLibrary.simpleMessage("Choose private key"),
"clear": MessageLookupByLibrary.simpleMessage("Clear"),
"clickSee": MessageLookupByLibrary.simpleMessage("Click here"),
"close": MessageLookupByLibrary.simpleMessage("Close"),
"cmd": MessageLookupByLibrary.simpleMessage("Command"),
"containerStatus":
MessageLookupByLibrary.simpleMessage("Container status"),
"convert": MessageLookupByLibrary.simpleMessage("Convert"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"copyPath": MessageLookupByLibrary.simpleMessage("Copy path"),
"createFile": MessageLookupByLibrary.simpleMessage("Create file"),
"createFolder": MessageLookupByLibrary.simpleMessage("Create folder"),
"currentMode": MessageLookupByLibrary.simpleMessage("Current Mode"),
"debug": MessageLookupByLibrary.simpleMessage("Debug"),
"decode": MessageLookupByLibrary.simpleMessage("Decode"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
"disconnected": MessageLookupByLibrary.simpleMessage("Disconnected"),
"dl2Local": m0,
"dockerContainerName":
MessageLookupByLibrary.simpleMessage("Container name"),
"dockerEditHost":
MessageLookupByLibrary.simpleMessage("Edit DOCKER_HOST"),
"dockerEmptyRunningItems": MessageLookupByLibrary.simpleMessage(
"No running container. \nIt may be that the env DOCKER_HOST is not read correctly. You can found it by running `echo \$DOCKER_HOST` in terminal."),
"dockerImage": MessageLookupByLibrary.simpleMessage("Image"),
"dockerImagesFmt": m1,
"dockerNotInstalled":
MessageLookupByLibrary.simpleMessage("Docker not installed"),
"dockerStatusRunningAndStoppedFmt": m2,
"dockerStatusRunningFmt": m3,
"download": MessageLookupByLibrary.simpleMessage("Download"),
"downloadFinished":
MessageLookupByLibrary.simpleMessage("Download finished"),
"downloadStatus": m4,
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"encode": MessageLookupByLibrary.simpleMessage("Encode"),
"error": MessageLookupByLibrary.simpleMessage("Error"),
"exampleName": MessageLookupByLibrary.simpleMessage("Example name"),
"experimentalFeature":
MessageLookupByLibrary.simpleMessage("Experimental feature"),
"export": MessageLookupByLibrary.simpleMessage("Export"),
"extraArgs": MessageLookupByLibrary.simpleMessage("Extra args"),
"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,
"go": MessageLookupByLibrary.simpleMessage("Go"),
"goSftpDlPage":
MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"),
"goto": MessageLookupByLibrary.simpleMessage("Go to"),
"host": MessageLookupByLibrary.simpleMessage("Host"),
"httpFailedWithCode": m6,
"imagesList": MessageLookupByLibrary.simpleMessage("Images list"),
"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."),
"invalidJson": MessageLookupByLibrary.simpleMessage("Invalid JSON"),
"invalidVersion":
MessageLookupByLibrary.simpleMessage("Invalid version"),
"invalidVersionHelp": m7,
"isBusy": MessageLookupByLibrary.simpleMessage("Is busy now"),
"keepForeground":
MessageLookupByLibrary.simpleMessage("Keep app foreground!"),
"keyAuth": MessageLookupByLibrary.simpleMessage("Key Auth"),
"lastTry": MessageLookupByLibrary.simpleMessage("Last try!"),
"launchPage": MessageLookupByLibrary.simpleMessage("Launch page"),
"license": MessageLookupByLibrary.simpleMessage("License"),
"loadingFiles":
MessageLookupByLibrary.simpleMessage("Loading files..."),
"loss": MessageLookupByLibrary.simpleMessage("loss"),
"madeWithLove": m8,
"max": MessageLookupByLibrary.simpleMessage("max"),
"min": MessageLookupByLibrary.simpleMessage("min"),
"ms": MessageLookupByLibrary.simpleMessage("ms"),
"name": MessageLookupByLibrary.simpleMessage("Name"),
"newContainer": MessageLookupByLibrary.simpleMessage("New container"),
"noClient": MessageLookupByLibrary.simpleMessage("No client"),
"noInterface": MessageLookupByLibrary.simpleMessage("No interface"),
"noResult": MessageLookupByLibrary.simpleMessage("No result"),
"noSavedPrivateKey":
MessageLookupByLibrary.simpleMessage("No saved private keys."),
"noSavedSnippet":
MessageLookupByLibrary.simpleMessage("No saved snippets."),
"noServerAvailable":
MessageLookupByLibrary.simpleMessage("No server available."),
"noUpdateAvailable":
MessageLookupByLibrary.simpleMessage("No update available"),
"ok": MessageLookupByLibrary.simpleMessage("OK"),
"onServerDetailPage":
MessageLookupByLibrary.simpleMessage("On server detail page"),
"open": MessageLookupByLibrary.simpleMessage("Open"),
"path": MessageLookupByLibrary.simpleMessage("Path"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"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."),
"plzSelectKey":
MessageLookupByLibrary.simpleMessage("Please select a key."),
"port": MessageLookupByLibrary.simpleMessage("Port"),
"preview": MessageLookupByLibrary.simpleMessage("Preview"),
"privateKey": MessageLookupByLibrary.simpleMessage("Private Key"),
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
"reportBugsOnGithubIssue": m9,
"restore": MessageLookupByLibrary.simpleMessage("Restore"),
"restoreSuccess": MessageLookupByLibrary.simpleMessage(
"Restore success. Restart app to apply."),
"restoreSureWithDate": m10,
"result": MessageLookupByLibrary.simpleMessage("Result"),
"run": MessageLookupByLibrary.simpleMessage("Run"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"second": MessageLookupByLibrary.simpleMessage("s"),
"server": MessageLookupByLibrary.simpleMessage("Server"),
"serverTabConnecting":
MessageLookupByLibrary.simpleMessage("Connecting..."),
"serverTabEmpty": MessageLookupByLibrary.simpleMessage(
"There is no server.\nClick the fab to add one."),
"serverTabFailed": MessageLookupByLibrary.simpleMessage("Failed"),
"serverTabLoading": MessageLookupByLibrary.simpleMessage("Loading..."),
"serverTabPlzSave": MessageLookupByLibrary.simpleMessage(
"Please \'save\' this private key again."),
"serverTabUnkown":
MessageLookupByLibrary.simpleMessage("Unknown state"),
"setting": MessageLookupByLibrary.simpleMessage("Setting"),
"sftpDlPrepare":
MessageLookupByLibrary.simpleMessage("Preparing to connect..."),
"sftpNoDownloadTask":
MessageLookupByLibrary.simpleMessage("No download task."),
"sftpSSHConnected":
MessageLookupByLibrary.simpleMessage("SFTP Connected"),
"showDistLogo":
MessageLookupByLibrary.simpleMessage("Show distribution logo"),
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
"spentTime": m11,
"sshTip": m12,
"start": MessageLookupByLibrary.simpleMessage("Start"),
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
"sureDelete": m13,
"sureNoPwd": MessageLookupByLibrary.simpleMessage(
"Are you sure to use no password?"),
"sureToDeleteServer": m14,
"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.\nCan\'t calculate CPU status."),
"updateServerStatusInterval": MessageLookupByLibrary.simpleMessage(
"Server status update interval"),
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
"user": MessageLookupByLibrary.simpleMessage("User"),
"versionHaveUpdate": m15,
"versionUnknownUpdate": m16,
"versionUpdated": m17,
"waitConnection": MessageLookupByLibrary.simpleMessage(
"Please wait for the connection to be established."),
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("Will take effect immediately")
};
}

View File

@@ -1,228 +0,0 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a zh locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'zh';
static String m0(fileName) => "下载 [${fileName}] 到本地?";
static String m1(count) => "${count} 个镜像";
static String m2(runningCount, stoppedCount) =>
"${runningCount}个正在运行, ${stoppedCount}个已停止";
static String m3(count) => "${count}个容器正在运行";
static String m4(percent, size) => "${size}${percent}%";
static String m5(count) => "找到 ${count} 个更新";
static String m6(code) => "请求失败, 状态码: ${code}";
static String m7(url) =>
"请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 ${url} 提交问题。";
static String m8(myGithub) => "\n用❤️制作 by ${myGithub}";
static String m9(url) => "请到 ${url} 提交问题";
static String m10(date) => "确定恢复 ${date} 的备份吗?";
static String m11(time) => "耗时: ${time}";
static String m12(url) => "该功能目前处于测试阶段,请在 ${url} 反馈问题,或者加入我们开发。";
static String m13(name) => "确定删除[${name}]";
static String m14(server) => "你确定要删除服务器 [${server}] 吗?";
static String m15(build) => "找到新版本v1.0.${build}, 点击更新";
static String m16(build) => "当前v1.0.${build}";
static String m17(build) => "当前v1.0.${build}, 已是最新版本";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks":
MessageLookupByLibrary.simpleMessage("\n保留所有权利。\n\n感谢以下参与软件测试的各位。"),
"addAServer": MessageLookupByLibrary.simpleMessage("添加服务器"),
"addOne": 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("关闭"),
"cmd": MessageLookupByLibrary.simpleMessage("命令"),
"containerStatus": MessageLookupByLibrary.simpleMessage("容器状态"),
"convert": MessageLookupByLibrary.simpleMessage("转换"),
"copy": MessageLookupByLibrary.simpleMessage("复制到剪切板"),
"copyPath": MessageLookupByLibrary.simpleMessage("复制路径"),
"createFile": MessageLookupByLibrary.simpleMessage("创建文件"),
"createFolder": MessageLookupByLibrary.simpleMessage("创建文件夹"),
"currentMode": MessageLookupByLibrary.simpleMessage("当前模式"),
"debug": MessageLookupByLibrary.simpleMessage("调试"),
"decode": MessageLookupByLibrary.simpleMessage("解码"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"disconnected": MessageLookupByLibrary.simpleMessage("连接断开"),
"dl2Local": m0,
"dockerContainerName": MessageLookupByLibrary.simpleMessage("容器名"),
"dockerEditHost":
MessageLookupByLibrary.simpleMessage("编辑 DOCKER_HOST"),
"dockerEmptyRunningItems": MessageLookupByLibrary.simpleMessage(
"没有正在运行的容器。\n这可能是因为环境变量 DOCKER_HOST 没有被正确读取。你可以通过在终端内运行 `echo \$DOCKER_HOST` 来获取。"),
"dockerImage": MessageLookupByLibrary.simpleMessage("镜像"),
"dockerImagesFmt": m1,
"dockerNotInstalled": MessageLookupByLibrary.simpleMessage("Docker未安装"),
"dockerStatusRunningAndStoppedFmt": m2,
"dockerStatusRunningFmt": m3,
"download": MessageLookupByLibrary.simpleMessage("下载"),
"downloadFinished": MessageLookupByLibrary.simpleMessage("下载完成!"),
"downloadStatus": m4,
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"encode": MessageLookupByLibrary.simpleMessage("编码"),
"error": MessageLookupByLibrary.simpleMessage("出错了"),
"exampleName": MessageLookupByLibrary.simpleMessage("名称示例"),
"experimentalFeature": MessageLookupByLibrary.simpleMessage("实验性功能"),
"export": MessageLookupByLibrary.simpleMessage("导出"),
"extraArgs": MessageLookupByLibrary.simpleMessage("额外参数"),
"feedback": MessageLookupByLibrary.simpleMessage("反馈"),
"feedbackOnGithub":
MessageLookupByLibrary.simpleMessage("如果你有任何问题请在GitHub反馈"),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage("这些输入框不能为空。"),
"files": MessageLookupByLibrary.simpleMessage("文件"),
"foundNUpdate": m5,
"go": MessageLookupByLibrary.simpleMessage("开始"),
"goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"),
"goto": MessageLookupByLibrary.simpleMessage("前往"),
"host": MessageLookupByLibrary.simpleMessage("主机"),
"httpFailedWithCode": m6,
"imagesList": MessageLookupByLibrary.simpleMessage("镜像列表"),
"import": MessageLookupByLibrary.simpleMessage("导入"),
"importAndExport": MessageLookupByLibrary.simpleMessage("导入或导出"),
"inputDomainHere": MessageLookupByLibrary.simpleMessage("在这里输入域名"),
"install": MessageLookupByLibrary.simpleMessage("安装"),
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
"请先 https://docs.docker.com/engine/install docker"),
"invalidJson": MessageLookupByLibrary.simpleMessage("无效的json存在格式问题"),
"invalidVersion": MessageLookupByLibrary.simpleMessage("不支持的版本"),
"invalidVersionHelp": m7,
"isBusy": MessageLookupByLibrary.simpleMessage("当前正忙"),
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
"lastTry": MessageLookupByLibrary.simpleMessage("最后尝试"),
"launchPage": MessageLookupByLibrary.simpleMessage("启动页"),
"license": MessageLookupByLibrary.simpleMessage("开源证书"),
"loadingFiles": MessageLookupByLibrary.simpleMessage("正在加载目录。。。"),
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
"madeWithLove": m8,
"max": MessageLookupByLibrary.simpleMessage("最大"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"ms": MessageLookupByLibrary.simpleMessage("毫秒"),
"name": MessageLookupByLibrary.simpleMessage("名称"),
"newContainer": MessageLookupByLibrary.simpleMessage("新建容器"),
"noClient": MessageLookupByLibrary.simpleMessage("没有SSH连接"),
"noInterface": MessageLookupByLibrary.simpleMessage("没有可用的接口"),
"noResult": MessageLookupByLibrary.simpleMessage("无结果"),
"noSavedPrivateKey": MessageLookupByLibrary.simpleMessage("没有已保存的私钥。"),
"noSavedSnippet": MessageLookupByLibrary.simpleMessage("没有已保存的代码片段。"),
"noServerAvailable": MessageLookupByLibrary.simpleMessage("没有可用的服务器。"),
"noUpdateAvailable": MessageLookupByLibrary.simpleMessage("没有可用更新"),
"ok": MessageLookupByLibrary.simpleMessage(""),
"onServerDetailPage": MessageLookupByLibrary.simpleMessage("在服务器详情页"),
"open": MessageLookupByLibrary.simpleMessage("打开"),
"path": MessageLookupByLibrary.simpleMessage("路径"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("平均:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"),
"pingNoServer": MessageLookupByLibrary.simpleMessage(
"没有服务器可用于Ping\n请在服务器tab添加服务器后再试"),
"platformNotSupportUpdate":
MessageLookupByLibrary.simpleMessage("当前平台不支持更新,请编译最新源码后手动安装"),
"plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"),
"plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"),
"port": MessageLookupByLibrary.simpleMessage("端口"),
"preview": MessageLookupByLibrary.simpleMessage("预览"),
"privateKey": MessageLookupByLibrary.simpleMessage("私钥"),
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
"reportBugsOnGithubIssue": m9,
"restore": MessageLookupByLibrary.simpleMessage("恢复"),
"restoreSuccess":
MessageLookupByLibrary.simpleMessage("恢复成功需要重启App来应用更改"),
"restoreSureWithDate": m10,
"result": MessageLookupByLibrary.simpleMessage("结果"),
"run": MessageLookupByLibrary.simpleMessage("运行"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"second": MessageLookupByLibrary.simpleMessage(""),
"server": MessageLookupByLibrary.simpleMessage("服务器"),
"serverTabConnecting": MessageLookupByLibrary.simpleMessage("连接中..."),
"serverTabEmpty":
MessageLookupByLibrary.simpleMessage("现在没有服务器。\n点击右下方按钮来添加。"),
"serverTabFailed": MessageLookupByLibrary.simpleMessage("失败"),
"serverTabLoading": MessageLookupByLibrary.simpleMessage("加载中..."),
"serverTabPlzSave": MessageLookupByLibrary.simpleMessage("请再次保存该私钥"),
"serverTabUnkown": MessageLookupByLibrary.simpleMessage("未知状态"),
"setting": MessageLookupByLibrary.simpleMessage("设置"),
"sftpDlPrepare": MessageLookupByLibrary.simpleMessage("准备连接至服务器..."),
"sftpNoDownloadTask": MessageLookupByLibrary.simpleMessage("没有下载任务"),
"sftpSSHConnected":
MessageLookupByLibrary.simpleMessage("SFTP 已连接,即将开始下载..."),
"showDistLogo": MessageLookupByLibrary.simpleMessage("显示发行版 Logo"),
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
"spentTime": m11,
"sshTip": m12,
"start": MessageLookupByLibrary.simpleMessage("开始"),
"stop": MessageLookupByLibrary.simpleMessage("停止"),
"sureDelete": m13,
"sureNoPwd": MessageLookupByLibrary.simpleMessage("确认使用无密码?"),
"sureToDeleteServer": m14,
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
"unkownConvertMode": MessageLookupByLibrary.simpleMessage("未知转换模式"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"updateAll": MessageLookupByLibrary.simpleMessage("更新全部"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"你设置为0服务器状态不会自动刷新。\n且不能计算CPU使用情况。"),
"updateServerStatusInterval":
MessageLookupByLibrary.simpleMessage("服务器状态刷新间隔"),
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
"user": MessageLookupByLibrary.simpleMessage("用户"),
"versionHaveUpdate": m15,
"versionUnknownUpdate": m16,
"versionUpdated": m17,
"waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"),
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
};
}

File diff suppressed because it is too large Load Diff

165
lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,165 @@
{
"about": "About",
"aboutThanks": "\n\nThanks to the following people who participated in the test.",
"addAServer": "add a server",
"addOne": "Add one",
"addPrivateKey": "Add private key",
"alreadyLastDir": "Already in last directory.",
"appPrimaryColor": "App primary color",
"attention": "Attention",
"auto": "Auto",
"backDir": "Back",
"backup": "Backup",
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).",
"backupVersionNotMatch": "Backup version is not match.",
"cancel": "Cancel",
"choose": "Choose",
"chooseDestination": "Choose destination",
"choosePrivateKey": "Choose private key",
"clear": "Clear",
"clickSee": "Click here",
"close": "Close",
"cmd": "Command",
"containerStatus": "Container status",
"convert": "Convert",
"copy": "Copy",
"copyPath": "Copy path",
"createFile": "Create file",
"createFolder": "Create folder",
"currentMode": "Current Mode",
"dark": "Dark",
"debug": "Debug",
"decode": "Decode",
"delete": "Delete",
"disconnected": "Disconnected",
"dl2Local": "Download [{fileName}] to local?",
"dockerContainerName": "Container name",
"dockerEditHost": "Edit DOCKER_HOST",
"dockerEmptyRunningItems": "No running container. \nIt may be that the env DOCKER_HOST is not read correctly. You can found it by running `echo $DOCKER_HOST` in terminal.",
"dockerImage": "Image",
"dockerImagesFmt": "{count} images",
"dockerNotInstalled": "Docker not installed",
"dockerStatusRunningAndStoppedFmt": "{runningCount} running, {stoppedCount} container stopped.",
"dockerStatusRunningFmt": "{count} container running.",
"download": "Download",
"downloadFinished": "Download finished",
"downloadStatus": "{percent}% of {size}",
"edit": "Edit",
"encode": "Encode",
"error": "Error",
"exampleName": "Example name",
"experimentalFeature": "Experimental feature",
"export": "Export",
"extraArgs": "Extra args",
"feedback": "Feedback",
"feedbackOnGithub": "If you have any questions, please feedback on Github.",
"fieldMustNotEmpty": "These fields must not be empty.",
"fileNotExist": "{file} not exist",
"fileTooLarge": "File '{file}' too large {size}, max {sizeMax}",
"files": "Files",
"foundNUpdate": "Found {count} update",
"go": "Go",
"goto": "Go to",
"host": "Host",
"httpFailedWithCode": "request failed, status code: {code}",
"imagesList": "Images list",
"import": "Import",
"importAndExport": "Import and Export",
"inputDomainHere": "Input Domain here",
"install": "install",
"installDockerWithUrl": "Please https://docs.docker.com/engine/install docker first.",
"invalidJson": "Invalid JSON",
"invalidVersion": "Invalid version",
"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}.",
"isBusy": "Is busy now",
"keepForeground": "Keep app foreground!",
"keyAuth": "Key Auth",
"lastTry": "Last try!",
"launchPage": "Launch page",
"license": "License",
"light": "Light",
"loadingFiles": "Loading files...",
"loss": "loss",
"madeWithLove": "\nMade with ❤️ by {myGithub}",
"max": "max",
"maxRetryCount": "Number of server reconnection",
"maxRetryCountEqual0": "Will retry again and again.",
"min": "min",
"ms": "ms",
"name": "Name",
"newContainer": "New container",
"noClient": "No client",
"noInterface": "No interface",
"noResult": "No result",
"noSavedPrivateKey": "No saved private keys.",
"noSavedSnippet": "No saved snippets.",
"noServerAvailable": "No server available.",
"noUpdateAvailable": "No update available",
"ok": "OK",
"onServerDetailPage": "On server detail page",
"open": "Open",
"path": "Path",
"pickFile": "Pick file",
"ping": "Ping",
"pingAvg": "Avg:",
"pingInputIP": "Please input a target IP/domain.",
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
"pkg": "Pkg",
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
"plzEnterHost": "Please enter host.",
"plzSelectKey": "Please select a key.",
"port": "Port",
"preview": "Preview",
"privateKey": "Private Key",
"pwd": "Password",
"rename": "Rename",
"reportBugsOnGithubIssue": "Please report bugs on {url}",
"restore": "Restore",
"restoreSuccess": "Restore success. Restart app to apply.",
"restoreSureWithDate": "Are you sure to restore from {date} ?",
"result": "Result",
"run": "Run",
"save": "Save",
"second": "s",
"server": "Server",
"serverTabConnecting": "Connecting...",
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
"serverTabFailed": "Failed",
"serverTabLoading": "Loading...",
"serverTabPlzSave": "Please 'save' this private key again.",
"serverTabUnkown": "Unknown state",
"setting": "Setting",
"sftpDlPrepare": "Preparing to connect...",
"sftpNoDownloadTask": "No download task.",
"sftpSSHConnected": "SFTP Connected",
"showDistLogo": "Show distribution logo",
"snippet": "Snippet",
"spentTime": "Spent time: {time}",
"sshTip": "This function is now in the experimental stage.\n\nPlease report bugs on {url} or join our development.",
"start": "Start",
"stop": "Stop",
"sureDelete": "Are you sure to delete [{name}]?",
"sureNoPwd": "Are you sure to use no password?",
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
"termTheme": "Terminal theme",
"themeMode": "Theme mode",
"times": "Times",
"ttl": "ttl",
"unknown": "unknown",
"unknownError": "Unknown error",
"unkownConvertMode": "Unknown convert mode",
"update": "Update",
"updateAll": "Update all",
"updateIntervalEqual0": "You set to 0, will not update automatically.\nCan't calculate CPU status.",
"updateServerStatusInterval": "Server status update interval",
"updateTip": "Update: v1.0.{newest}",
"updateTipTooLow": "Current version is too low, please update to v1.0.{newest}",
"upsideDown": "Upside Down",
"urlOrJson": "URL or JSON",
"user": "User",
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
"versionUnknownUpdate": "Current: v1.0.{build}",
"versionUpdated": "Current: v1.0.{build}, is up to date",
"waitConnection": "Please wait for the connection to be established.",
"willTakEeffectImmediately": "Will take effect immediately"
}

165
lib/l10n/app_zh.arb Normal file
View File

@@ -0,0 +1,165 @@
{
"about": "关于",
"aboutThanks": "\n\n感谢以下参与软件测试的各位。",
"addAServer": "添加服务器",
"addOne": "前去新增",
"addPrivateKey": "添加一个私钥",
"alreadyLastDir": "已经是最上层目录了",
"appPrimaryColor": "App主要色",
"attention": "注意",
"auto": "自动",
"backDir": "返回上一级",
"backup": "备份",
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。\n除了设置项恢复的数据不会覆盖现有数据。",
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
"cancel": "取消",
"choose": "选择",
"chooseDestination": "选择目标",
"choosePrivateKey": "选择私钥",
"clear": "清除",
"clickSee": "点击查看",
"close": "关闭",
"cmd": "命令",
"containerStatus": "容器状态",
"convert": "转换",
"copy": "复制",
"copyPath": "复制路径",
"createFile": "创建文件",
"createFolder": "创建文件夹",
"currentMode": "当前模式",
"dark": "暗",
"debug": "调试",
"decode": "解码",
"delete": "删除",
"disconnected": "连接断开",
"dl2Local": "下载 [{fileName}] 到本地?",
"dockerContainerName": "容器名",
"dockerEditHost": "编辑 DOCKER_HOST",
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为环境变量 DOCKER_HOST 没有被正确读取。你可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
"dockerImage": "镜像",
"dockerImagesFmt": "共 {count} 个镜像",
"dockerNotInstalled": "Docker未安装",
"dockerStatusRunningAndStoppedFmt": "{runningCount}个正在运行, {stoppedCount}个已停止",
"dockerStatusRunningFmt": "{count}个容器正在运行",
"download": "下载",
"downloadFinished": "下载完成!",
"downloadStatus": "{size} 的 {percent}%",
"edit": "编辑",
"encode": "编码",
"error": "出错了",
"exampleName": "名称示例",
"experimentalFeature": "实验性功能",
"export": "导出",
"extraArgs": "额外参数",
"feedback": "反馈",
"feedbackOnGithub": "如果你有任何问题请在GitHub反馈",
"fieldMustNotEmpty": "这些输入框不能为空。",
"fileNotExist": "{file} 不存在",
"fileTooLarge": "文件 '{file}' 过大 '{size}',超过了 {sizeMax}",
"files": "文件",
"foundNUpdate": "找到 {count} 个更新",
"go": "开始",
"goto": "前往",
"host": "主机",
"httpFailedWithCode": "请求失败, 状态码: {code}",
"imagesList": "镜像列表",
"import": "导入",
"importAndExport": "导入或导出",
"inputDomainHere": "在这里输入域名",
"install": "安装",
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
"invalidJson": "无效的json存在格式问题",
"invalidVersion": "不支持的版本",
"invalidVersionHelp": "请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 {url} 提交问题。",
"isBusy": "当前正忙",
"keepForeground": "请保持应用处于前台!",
"keyAuth": "公钥认证",
"lastTry": "最后尝试",
"launchPage": "启动页",
"license": "开源证书",
"light": "亮",
"loadingFiles": "正在加载目录。。。",
"loss": "丢包率",
"madeWithLove": "\n用❤制作 by {myGithub}",
"max": "最大",
"maxRetryCount": "服务器尝试重连次数",
"maxRetryCountEqual0": "会无限重试",
"min": "最小",
"ms": "毫秒",
"name": "名称",
"newContainer": "新建容器",
"noClient": "没有SSH连接",
"noInterface": "没有可用的接口",
"noResult": "无结果",
"noSavedPrivateKey": "没有已保存的私钥。",
"noSavedSnippet": "没有已保存的代码片段。",
"noServerAvailable": "没有可用的服务器。",
"noUpdateAvailable": "没有可用更新",
"ok": "好",
"onServerDetailPage": "在服务器详情页",
"open": "打开",
"path": "路径",
"pickFile": "选择文件",
"ping": "Ping",
"pingAvg": "平均:",
"pingInputIP": "请输入目标IP或域名",
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
"pkg": "包管理",
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
"plzEnterHost": "请输入主机",
"plzSelectKey": "请选择私钥",
"port": "端口",
"preview": "预览",
"privateKey": "私钥",
"pwd": "密码",
"rename": "重命名",
"reportBugsOnGithubIssue": "请到 {url} 提交问题",
"restore": "恢复",
"restoreSuccess": "恢复成功需要重启App来应用更改",
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
"result": "结果",
"run": "运行",
"save": "保存",
"second": "秒",
"server": "服务器",
"serverTabConnecting": "连接中...",
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
"serverTabFailed": "失败",
"serverTabLoading": "加载中...",
"serverTabPlzSave": "请再次保存该私钥",
"serverTabUnkown": "未知状态",
"setting": "设置",
"sftpDlPrepare": "准备连接至服务器...",
"sftpNoDownloadTask": "没有下载任务",
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
"showDistLogo": "显示发行版 Logo",
"snippet": "代码片段",
"spentTime": "耗时: {time}",
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
"start": "开始",
"stop": "停止",
"sureDelete": "确定删除[{name}]",
"sureNoPwd": "确认使用无密码?",
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
"termTheme": "终端主题",
"themeMode": "主题模式",
"times": "次",
"ttl": "缓存时间",
"unknown": "未知",
"unknownError": "未知错误",
"unkownConvertMode": "未知转换模式",
"update": "更新",
"updateAll": "更新全部",
"updateIntervalEqual0": "你设置为0服务器状态不会自动刷新。\n且不能计算CPU使用情况。",
"updateServerStatusInterval": "服务器状态刷新间隔",
"updateTip": "新版本: v1.0.{newest}",
"updateTipTooLow": "当前版本过低,请升级至 v1.0.{newest}",
"upsideDown": "上下交换",
"urlOrJson": "链接或JSON",
"user": "用户",
"versionHaveUpdate": "找到新版本v1.0.{build}, 点击更新",
"versionUnknownUpdate": "当前v1.0.{build}",
"versionUpdated": "当前v1.0.{build}, 已是最新版本",
"waitConnection": "请等待连接建立",
"willTakEeffectImmediately": "更改将会立即生效"
}

View File

@@ -1,151 +0,0 @@
{
"server": "Server",
"convert": "Convert",
"ping": "Ping",
"debug": "Debug",
"addAServer": "add a server",
"setting": "Setting",
"license": "License",
"snippet": "Snippet",
"privateKey": "Private Key",
"madeWithLove": "\nMade with ❤️ by {myGithub}",
"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.",
"serverTabFailed": "Failed",
"serverTabUnkown": "Unknown state",
"serverTabConnecting": "Connecting...",
"decode": "Decode",
"encode": "Encode",
"currentMode": "Current Mode",
"unkownConvertMode": "Unknown convert mode",
"copy": "Copy",
"upsideDown": "Upside Down",
"pingAvg": "Avg:",
"unknown": "unknown",
"min": "min",
"max": "max",
"ms": "ms",
"ttl": "ttl",
"loss": "loss",
"noResult": "No result",
"pingInputIP": "Please input a target IP/domain.",
"clear": "Clear",
"start": "Start",
"appPrimaryColor": "App primary color",
"updateServerStatusInterval": "Server status update interval",
"willTakEeffectImmediately": "Will take effect immediately",
"launchPage": "Launch page",
"versionUpdated": "Current: v1.0.{build}, is up to date",
"versionUnknownUpdate": "Current: v1.0.{build}",
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
"second": "s",
"updateIntervalEqual0": "You set to 0, will not update automatically.\nCan't calculate CPU status.",
"edit": "Edit",
"noSavedPrivateKey": "No saved private keys.",
"name": "Name",
"pwd": "Password",
"save": "Save",
"delete": "Delete",
"fieldMustNotEmpty": "These fields must not be empty.",
"importAndExport": "Import and Export",
"choose": "Choose",
"import": "Import",
"export": "Export",
"ok": "OK",
"cancel": "Cancel",
"urlOrJson": "URL or JSON",
"go": "Go",
"httpFailedWithCode": "request failed, status code: {code}",
"run": "Run",
"noSavedSnippet": "No saved snippets.",
"chooseDestination": "Choose destination",
"noServerAvailable": "No server available.",
"result": "Result",
"close": "Close",
"attention": "Attention",
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
"host": "Host",
"port": "Port",
"user": "User",
"keyAuth": "Key Auth",
"addPrivateKey": "Add private key",
"choosePrivateKey": "Choose private key",
"plzEnterHost": "Please enter host.",
"sureNoPwd": "Are you sure to use no password?",
"plzSelectKey": "Please select a key.",
"exampleName": "Example name",
"stop": "Stop",
"download": "Download",
"copyPath": "Copy path",
"keepForeground": "Keep app foreground!",
"downloadFinished": "Download finished",
"downloadStatus": "{percent}% of {size}",
"sftpDlPrepare": "Preparing to connect...",
"sftpSSHConnected": "SFTP Connected",
"spentTime": "Spent time: {time}",
"backDir": "Back",
"alreadyLastDir": "Already in last directory.",
"open": "Open",
"sureDelete": "Are you sure to delete [{name}]?",
"containerStatus": "Container status",
"noClient": "No client",
"installDockerWithUrl": "Please https://docs.docker.com/engine/install docker first.",
"waitConnection": "Please wait for the connection to be established.",
"unknownError": "Unknown error",
"dockerStatusRunningFmt": "{count} container running.",
"dockerStatusRunningAndStoppedFmt": "{runningCount} running, {stoppedCount} container stopped.",
"install": "install",
"loadingFiles": "Loading files...",
"sftpNoDownloadTask": "No download task.",
"goSftpDlPage": "Go to SFTP download page?",
"createFolder": "Create folder",
"createFile": "Create file",
"rename": "Rename",
"dl2Local": "Download [{fileName}] to local?",
"error": "Error",
"disconnected": "Disconnected",
"files": "Files",
"experimentalFeature": "Experimental feature",
"reportBugsOnGithubIssue": "Please report bugs on {url}",
"noUpdateAvailable": "No update available",
"foundNUpdate": "Found {count} update",
"updateAll": "Update all",
"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!",
"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",
"dockerNotInstalled": "Docker not installed",
"invalidVersion": "Invalid version",
"cmd": "Command",
"dockerEmptyRunningItems": "No running container. \nIt may be that the env DOCKER_HOST is not read correctly. You can found it by running `echo $DOCKER_HOST` in terminal.",
"dockerEditHost": "Edit DOCKER_HOST",
"newContainer": "New container",
"dockerImage": "Image",
"dockerContainerName": "Container name",
"extraArgs": "Extra args",
"preview": "Preview",
"isBusy": "Is busy now",
"imagesList": "Images list",
"dockerImagesFmt": "{count} images",
"path": "Path",
"goto": "Go to",
"showDistLogo": "Show distribution logo",
"onServerDetailPage": "On server detail page",
"addOne": "Add one",
"sshTip": "This function is now in the experimental stage. \nPlease report bugs on {url} or join our development."
}

View File

@@ -1,151 +0,0 @@
{
"server": "服务器",
"convert": "转换",
"ping": "Ping",
"debug": "调试",
"addAServer": "添加服务器",
"setting": "设置",
"license": "开源证书",
"snippet": "代码片段",
"privateKey": "私钥",
"madeWithLove": "\n用❤制作 by {myGithub}",
"aboutThanks": "\n保留所有权利。\n\n感谢以下参与软件测试的各位。",
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
"serverTabLoading": "加载中...",
"serverTabPlzSave": "请再次保存该私钥",
"serverTabFailed": "失败",
"serverTabUnkown": "未知状态",
"serverTabConnecting": "连接中...",
"decode": "解码",
"encode": "编码",
"currentMode": "当前模式",
"unkownConvertMode": "未知转换模式",
"copy": "复制到剪切板",
"upsideDown": "上下交换",
"pingAvg": "平均:",
"unknown": "未知",
"min": "最小",
"max": "最大",
"ms": "毫秒",
"ttl": "缓存时间",
"loss": "丢包率",
"noResult": "无结果",
"pingInputIP": "请输入目标IP或域名",
"clear": "清除",
"start": "开始",
"appPrimaryColor": "App主要色",
"updateServerStatusInterval": "服务器状态刷新间隔",
"willTakEeffectImmediately": "更改将会立即生效",
"launchPage": "启动页",
"versionUpdated": "当前v1.0.{build}, 已是最新版本",
"versionUnknownUpdate": "当前v1.0.{build}",
"versionHaveUpdate": "找到新版本v1.0.{build}, 点击更新",
"second": "秒",
"updateIntervalEqual0": "你设置为0服务器状态不会自动刷新。\n且不能计算CPU使用情况。",
"edit": "编辑",
"noSavedPrivateKey": "没有已保存的私钥。",
"name": "名称",
"pwd": "密码",
"save": "保存",
"delete": "删除",
"fieldMustNotEmpty": "这些输入框不能为空。",
"importAndExport": "导入或导出",
"choose": "选择",
"import": "导入",
"export": "导出",
"ok": "好",
"cancel": "取消",
"urlOrJson": "链接或JSON",
"go": "开始",
"httpFailedWithCode": "请求失败, 状态码: {code}",
"run": "运行",
"noSavedSnippet": "没有已保存的代码片段。",
"chooseDestination": "选择目标",
"noServerAvailable": "没有可用的服务器。",
"result": "结果",
"close": "关闭",
"attention": "注意",
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
"host": "主机",
"port": "端口",
"user": "用户",
"keyAuth": "公钥认证",
"addPrivateKey": "添加一个私钥",
"choosePrivateKey": "选择私钥",
"plzEnterHost": "请输入主机",
"sureNoPwd": "确认使用无密码?",
"plzSelectKey": "请选择私钥",
"exampleName": "名称示例",
"stop": "停止",
"download": "下载",
"copyPath": "复制路径",
"keepForeground": "请保持应用处于前台!",
"downloadFinished": "下载完成!",
"downloadStatus": "{size} 的 {percent}%",
"sftpDlPrepare": "准备连接至服务器...",
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
"spentTime": "耗时: {time}",
"backDir": "返回上一级",
"alreadyLastDir": "已经是最上层目录了",
"open": "打开",
"sureDelete": "确定删除[{name}]",
"containerStatus": "容器状态",
"noClient": "没有SSH连接",
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
"waitConnection": "请等待连接建立",
"unknownError": "未知错误",
"dockerStatusRunningFmt": "{count}个容器正在运行",
"dockerStatusRunningAndStoppedFmt": "{runningCount}个正在运行, {stoppedCount}个已停止",
"install": "安装",
"loadingFiles": "正在加载目录。。。",
"sftpNoDownloadTask": "没有下载任务",
"goSftpDlPage": "前往下载页?",
"createFolder": "创建文件夹",
"createFile": "创建文件",
"rename": "重命名",
"dl2Local": "下载 [{fileName}] 到本地?",
"error": "出错了",
"disconnected": "连接断开",
"files": "文件",
"experimentalFeature": "实验性功能",
"reportBugsOnGithubIssue": "请到 {url} 提交问题",
"noUpdateAvailable": "没有可用更新",
"foundNUpdate": "找到 {count} 个更新",
"updateAll": "更新全部",
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
"invalidVersionHelp": "请确保正确安装了docker或者使用的非自编译版本。如果没有以上问题请在 {url} 提交问题。",
"noInterface": "没有可用的接口",
"lastTry": "最后尝试",
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。\n除了设置项恢复的数据不会覆盖现有数据。",
"backup": "备份",
"restore": "恢复",
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
"invalidJson": "无效的json存在格式问题",
"restoreSuccess": "恢复成功需要重启App来应用更改",
"clickSee": "点击查看",
"feedback": "反馈",
"feedbackOnGithub": "如果你有任何问题请在GitHub反馈",
"update": "更新",
"inputDomainHere": "在这里输入域名",
"dockerNotInstalled": "Docker未安装",
"invalidVersion": "不支持的版本",
"cmd": "命令",
"dockerEmptyRunningItems": "没有正在运行的容器。\n这可能是因为环境变量 DOCKER_HOST 没有被正确读取。你可以通过在终端内运行 `echo $DOCKER_HOST` 来获取。",
"dockerEditHost": "编辑 DOCKER_HOST",
"newContainer": "新建容器",
"dockerImage": "镜像",
"dockerContainerName": "容器名",
"extraArgs": "额外参数",
"preview": "预览",
"isBusy": "当前正忙",
"imagesList": "镜像列表",
"dockerImagesFmt": "共 {count} 个镜像",
"path": "路径",
"goto": "前往",
"showDistLogo": "显示发行版 Logo",
"onServerDetailPage": "在服务器详情页",
"addOne": "前去新增",
"sshTip": "该功能目前处于测试阶段,请在 {url} 反馈问题,或者加入我们开发。"
}

View File

@@ -1,18 +1,20 @@
import 'package:get_it/get_it.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/pkg.dart';
import 'package:toolbox/data/provider/debug.dart';
import 'package:toolbox/data/provider/docker.dart';
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/service/app.dart';
import 'package:toolbox/data/store/docker.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 'data/provider/app.dart';
import 'data/provider/debug.dart';
import 'data/provider/docker.dart';
import 'data/provider/pkg.dart';
import 'data/provider/private_key.dart';
import 'data/provider/server.dart';
import 'data/provider/sftp_download.dart';
import 'data/provider/snippet.dart';
import 'data/provider/virtual_keyboard.dart';
import 'data/service/app.dart';
import 'data/store/docker.dart';
import 'data/store/private_key.dart';
import 'data/store/server.dart';
import 'data/store/setting.dart';
import 'data/store/snippet.dart';
GetIt locator = GetIt.instance;
@@ -26,6 +28,7 @@ void setupLocatorForProviders() {
locator.registerSingleton(DebugProvider());
locator.registerSingleton(DockerProvider());
locator.registerSingleton(ServerProvider());
locator.registerSingleton(VirtualKeyboard());
locator.registerSingleton(SnippetProvider());
locator.registerSingleton(PrivateKeyProvider());
locator.registerSingleton(SftpDownloadProvider());

View File

@@ -4,31 +4,30 @@ import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
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/pkg.dart';
import 'package:toolbox/data/provider/debug.dart';
import 'package:toolbox/data/provider/docker.dart';
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';
import 'app.dart';
import 'core/analysis.dart';
import 'data/model/server/private_key_info.dart';
import 'data/model/server/server_private_info.dart';
import 'data/model/server/snippet.dart';
import 'data/provider/app.dart';
import 'data/provider/debug.dart';
import 'data/provider/docker.dart';
import 'data/provider/pkg.dart';
import 'data/provider/private_key.dart';
import 'data/provider/server.dart';
import 'data/provider/sftp_download.dart';
import 'data/provider/snippet.dart';
import 'data/provider/virtual_keyboard.dart';
import 'locator.dart';
late final DebugProvider _debug;
Future<void> initApp() async {
await initHive();
await setupLocator();
await upgradeStore();
_debug = locator<DebugProvider>();
locator<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData();
@@ -47,20 +46,6 @@ Future<void> initHive() async {
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) {
@@ -69,8 +54,7 @@ void runInZone(dynamic Function() body) {
// `setState() or markNeedsBuild() called during build`
// error.
Future.delayed(const Duration(milliseconds: 1), () {
final debugProvider = locator<DebugProvider>();
debugProvider.addText(line);
_debug.addText(line);
});
},
);
@@ -84,9 +68,8 @@ void runInZone(dynamic Function() body) {
void onError(Object obj, StackTrace stack) {
Analysis.recordException(obj);
final debugProvider = locator<DebugProvider>();
debugProvider.addError(obj);
debugProvider.addError(stack);
_debug.addMultiline(obj, Colors.red);
_debug.addMultiline(stack, Colors.white);
}
Future<void> main() async {
@@ -101,11 +84,12 @@ Future<void> main() async {
ChangeNotifierProvider(create: (_) => locator<DockerProvider>()),
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
ChangeNotifierProvider(create: (_) => locator<VirtualKeyboard>()),
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
ChangeNotifierProvider(
create: (_) => locator<SftpDownloadProvider>()),
],
child: const MyApp(),
child: MyApp(),
),
);
});

View File

@@ -4,17 +4,18 @@ 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:flutter_gen/gen_l10n/l10n.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';
import '../../core/extension/colorx.dart';
import '../../core/utils/ui.dart';
import '../../data/model/app/backup.dart';
import '../../data/res/font_style.dart';
import '../../data/store/private_key.dart';
import '../../data/store/server.dart';
import '../../data/store/setting.dart';
import '../../data/store/snippet.dart';
import '../../locator.dart';
const backupFormatVersion = 1;
@@ -29,7 +30,7 @@ class BackupPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context);
final s = S.of(context);
final s = S.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(s.importAndExport, style: textSize18),
@@ -67,13 +68,12 @@ class BackupPage extends StatelessWidget {
Widget _buildCard(String text, IconData icon, MediaQueryData media,
FutureOr Function() onTap) {
final priColor = primaryColor;
final textColor = priColor.isBrightColor ? Colors.black : Colors.white;
final textColor = primaryColor.isBrightColor ? Colors.black : Colors.white;
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(37), color: priColor),
borderRadius: BorderRadius.circular(37), color: primaryColor),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
child: Row(

View File

@@ -2,11 +2,12 @@ import 'dart:convert';
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';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../core/utils/ui.dart';
import '../../data/res/color.dart';
import '../widget/input_field.dart';
import '../widget/round_rect_card.dart';
class ConvertPage extends StatefulWidget {
const ConvertPage({Key? key}) : super(key: key);
@@ -37,7 +38,7 @@ class _ConvertPageState extends State<ConvertPage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -51,7 +52,7 @@ class _ConvertPageState extends State<ConvertPage>
children: [
const SizedBox(height: 13),
_buildInputTop(),
_buildTypeOption(),
_buildMiddleBtns(),
_buildResult(),
],
),
@@ -95,7 +96,7 @@ class _ConvertPageState extends State<ConvertPage>
);
}
Widget _buildTypeOption() {
Widget _buildMiddleBtns() {
final decode = _s.decode;
final encode = _s.encode;
final List<String> typeOption = [
@@ -111,8 +112,6 @@ class _ConvertPageState extends State<ConvertPage>
title: Row(
children: [
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Icon(Icons.change_circle, semanticLabel: _s.upsideDown),
onPressed: () {
final temp = _textEditingController.text;
@@ -121,15 +120,13 @@ class _ConvertPageState extends State<ConvertPage>
},
),
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor),
),
child: Icon(Icons.copy, semanticLabel: _s.copy),
onPressed: () => Clipboard.setData(
ClipboardData(
text: _textEditingControllerResult.text == ''
? ' '
: _textEditingControllerResult.text),
text: _textEditingControllerResult.text == ''
? ' '
: _textEditingControllerResult.text,
),
),
)
],
@@ -145,9 +142,10 @@ class _ConvertPageState extends State<ConvertPage>
textScaleFactor: 1.0,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: primaryColor),
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: primaryColor,
),
),
Text(
_s.currentMode,

View File

@@ -1,21 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/menu_item.dart';
import 'package:toolbox/data/model/docker/ps.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/error.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/data/store/docker.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/url_text.dart';
import '../../core/utils/ui.dart';
import '../../data/model/docker/ps.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/docker.dart';
import '../../data/provider/server.dart';
import '../../data/res/error.dart';
import '../../data/res/font_style.dart';
import '../../data/res/menu.dart';
import '../../data/res/url.dart';
import '../../data/store/docker.dart';
import '../../locator.dart';
import '../widget/center_loading.dart';
import '../widget/dropdown_menu.dart';
import '../widget/round_rect_card.dart';
import '../widget/two_line_text.dart';
import '../widget/url_text.dart';
class DockerManagePage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -39,7 +41,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -47,7 +49,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
super.initState();
final client = locator<ServerProvider>()
.servers
.firstWhere((element) => element.info == widget.spi)
.firstWhere((element) => element.spi == widget.spi)
.client;
if (client == null) {
showSnackBar(context, Text(_s.noClient));
@@ -59,32 +61,32 @@ class _DockerManagePageState extends State<DockerManagePage> {
@override
Widget build(BuildContext context) {
return Consumer<DockerProvider>(builder: (_, docker, __) {
return Consumer<DockerProvider>(builder: (_, ___, __) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: 'Docker', down: widget.spi.name),
actions: [
IconButton(
onPressed: () => docker.refresh(),
onPressed: () => _docker.refresh(),
icon: const Icon(Icons.refresh),
)
],
),
body: _buildMain(docker),
floatingActionButton: _buildFAB(docker),
body: _buildMain(),
floatingActionButton: _buildFAB(),
);
});
}
Widget _buildFAB(DockerProvider docker) {
Widget _buildFAB() {
return FloatingActionButton(
onPressed: () async => await _showAddFAB(docker),
onPressed: () async => await _showAddFAB(),
child: const Icon(Icons.add),
);
}
Future<void> _showAddFAB(DockerProvider docker) async {
Future<void> _showAddFAB() async {
final imageCtrl = TextEditingController();
final nameCtrl = TextEditingController();
final argsCtrl = TextEditingController();
@@ -188,7 +190,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
if (_textController.text == '') {
showRoundDialog(context, _s.attention, Text(_s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
),
]);
return;
}
@@ -229,9 +233,8 @@ class _DockerManagePageState extends State<DockerManagePage> {
return _textController.text.trim();
}
Widget _buildMain(DockerProvider docker) {
final running = docker.items;
if (docker.error != null && running == null) {
Widget _buildMain() {
if (_docker.error != null && _docker.items == null) {
return SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -242,17 +245,17 @@ class _DockerManagePageState extends State<DockerManagePage> {
size: 37,
),
const SizedBox(height: 27),
_buildErr(docker.error!),
_buildErr(_docker.error!),
const SizedBox(height: 27),
Padding(
padding: const EdgeInsets.all(17),
child: _buildSolution(docker.error!),
child: _buildSolution(_docker.error!),
)
],
),
);
}
if (running == null) {
if (_docker.items == null || _docker.images == null) {
_docker.refresh();
return centerLoading;
}
@@ -260,49 +263,74 @@ class _DockerManagePageState extends State<DockerManagePage> {
return ListView(
padding: const EdgeInsets.all(7),
children: [
_buildLoading(docker),
_buildLoading(),
_buildVersion(
docker.edition ?? _s.unknown, docker.version ?? _s.unknown),
_buildPsItems(running, docker),
_buildImages(docker),
_buildEditHost(running, docker),
_docker.edition ?? _s.unknown, _docker.version ?? _s.unknown),
_buildPsItems(),
_buildImages(),
_buildEditHost(),
].map((e) => RoundRectCard(e)).toList(),
);
}
Widget _buildImages(DockerProvider docker) {
if (docker.images == null) {
Widget _buildImages() {
if (_docker.images == null) {
return const SizedBox();
}
return ExpansionTile(
title: Text(_s.imagesList),
subtitle: Text(
_s.dockerImagesFmt(docker.images!.length),
style: grey,
),
children: docker.images!
.map(
(e) => ListTile(
title: Text(e.repo),
subtitle: Text('${e.tag} - ${e.size}'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
final result = await _docker.run('docker rmi ${e.id} -f');
if (result != null) {
showSnackBar(
context, Text(getErrMsg(result) ?? _s.unknownError));
}
},
),
title: Text(_s.imagesList),
subtitle: Text(
_s.dockerImagesFmt(_docker.images!.length),
style: grey,
),
children: _docker.images!
.map(
(e) => ListTile(
title: Text(e.repo),
subtitle: Text('${e.tag} - ${e.size}'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showRoundDialog(
context,
_s.attention,
Text(_s.sureDelete(e.repo)),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final result = await _docker.run(
'docker rmi ${e.id} -f',
);
if (result != null) {
showSnackBar(
context,
Text(getErrMsg(result) ?? _s.unknownError),
);
}
},
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
),
],
);
},
),
)
.toList());
),
)
.toList(),
);
}
Widget _buildLoading(DockerProvider docker) {
if (!docker.isBusy) return const SizedBox();
final haveLog = docker.runLog != null;
Widget _buildLoading() {
if (!_docker.isBusy) return const SizedBox();
final haveLog = _docker.runLog != null;
return Padding(
padding: const EdgeInsets.all(17),
child: Column(
@@ -311,14 +339,16 @@ class _DockerManagePageState extends State<DockerManagePage> {
child: CircularProgressIndicator(),
),
haveLog ? const SizedBox(height: 17) : const SizedBox(),
haveLog ? Text(docker.runLog!) : const SizedBox()
haveLog ? Text(_docker.runLog!) : const SizedBox()
],
),
);
}
Widget _buildEditHost(List<DockerPsItem> running, DockerProvider docker) {
if (running.isNotEmpty) return const SizedBox();
Widget _buildEditHost() {
if (_docker.items!.isNotEmpty || _docker.images!.isNotEmpty) {
return const SizedBox();
}
return Padding(
padding: const EdgeInsets.fromLTRB(17, 17, 17, 0),
child: Column(
@@ -328,7 +358,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => _showEditHostDialog(docker),
onPressed: () => _showEditHostDialog(),
child: Text(_s.dockerEditHost),
)
],
@@ -336,7 +366,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
);
}
Future<void> _showEditHostDialog(DockerProvider docker) async {
Future<void> _showEditHostDialog() async {
await showRoundDialog(
context,
_s.dockerEditHost,
@@ -347,7 +377,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
TextEditingController(text: 'unix:///run/user/1000/docker.sock'),
onSubmitted: (value) {
locator<DockerStore>().setDockerHost(widget.spi.id, value.trim());
docker.refresh();
_docker.refresh();
Navigator.of(context).pop();
},
),
@@ -407,34 +437,33 @@ class _DockerManagePageState extends State<DockerManagePage> {
);
}
Widget _buildPsItems(List<DockerPsItem> running, DockerProvider docker) {
Widget _buildPsItems() {
return ExpansionTile(
title: Text(_s.containerStatus),
subtitle: Text(_buildSubtitle(running), style: grey),
children: running.map(
subtitle: Text(_buildSubtitle(_docker.items!), style: grey),
children: _docker.items!.map(
(item) {
return ListTile(
title: Text(item.image),
subtitle: Text(item.status),
trailing:
_buildMoreBtn(item.running, item.containerId, docker.isBusy),
title: Text(item.name),
subtitle: Text('${item.image} - ${item.status}'),
trailing: _buildMoreBtn(item, _docker.isBusy),
);
},
).toList(),
);
}
Widget _buildMoreBtn(bool running, String containerId, bool busy) {
final item = running ? DockerMenuItems.stop : DockerMenuItems.start;
Widget _buildMoreBtn(DockerPsItem dItem, bool busy) {
final item = dItem.running ? DockerMenuItems.stop : DockerMenuItems.start;
return buildPopuopMenu(
items: [
PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
child: item.build(_s),
),
PopupMenuItem<DropdownBtnItem>(
value: DockerMenuItems.rm,
child: DockerMenuItems.rm.build,
child: DockerMenuItems.rm.build(_s),
),
],
onSelected: (value) {
@@ -445,13 +474,26 @@ class _DockerManagePageState extends State<DockerManagePage> {
final item = value as DropdownBtnItem;
switch (item) {
case DockerMenuItems.rm:
_docker.delete(containerId);
showRoundDialog(
context,
_s.attention,
Text(_s.sureDelete(dItem.name)),
[
TextButton(
onPressed: () {
Navigator.of(context).pop();
_docker.delete(dItem.containerId);
},
child: Text(_s.ok),
)
],
);
break;
case DockerMenuItems.start:
_docker.start(containerId);
_docker.start(dItem.containerId);
break;
case DockerMenuItems.stop:
_docker.stop(containerId);
_docker.stop(dItem.containerId);
break;
}
},

View File

@@ -1,36 +1,38 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_it/get_it.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/update.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/navigation_item.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/icon.dart';
import 'package:toolbox/data/res/tab.dart';
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';
import 'package:toolbox/view/page/private_key/list.dart';
import 'package:toolbox/view/page/server/tab.dart';
import 'package:toolbox/view/page/setting.dart';
import 'package:toolbox/view/page/sftp/downloaded.dart';
import 'package:toolbox/view/page/snippet/list.dart';
import 'package:toolbox/view/widget/url_text.dart';
import '../../core/analysis.dart';
import '../../core/route.dart';
import '../../core/update.dart';
import '../../core/utils/ui.dart';
import '../../data/model/app/dynamic_color.dart';
import '../../data/model/app/navigation_item.dart';
import '../../data/provider/server.dart';
import '../../data/res/build_data.dart';
import '../../data/res/font_style.dart';
import '../../data/res/icon.dart';
import '../../data/res/tab.dart';
import '../../data/res/url.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/url_text.dart';
import 'backup.dart';
import 'convert.dart';
import 'debug.dart';
import 'ping.dart';
import 'private_key/list.dart';
import 'server/tab.dart';
import 'setting.dart';
import 'sftp/downloaded.dart';
import 'snippet/list.dart';
final _bottomItemOverlayColor =
DynamicColor(Colors.black.withOpacity(0.07), Colors.white12);
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.primaryColor}) : super(key: key);
final Color primaryColor;
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
@@ -59,7 +61,7 @@ class _MyHomePageState extends State<MyHomePage>
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
_width = MediaQuery.of(context).size.width;
}
@@ -112,7 +114,7 @@ class _MyHomePageState extends State<MyHomePage>
);
}
Widget _buildItem(int idx, NavigationItem item, bool isSelected) {
Widget _buildBottomItem(int idx, NavigationItem item, bool isSelected) {
final width = _width / tabItems.length;
return AnimatedContainer(
duration: const Duration(milliseconds: 377),
@@ -121,9 +123,7 @@ class _MyHomePageState extends State<MyHomePage>
width: isSelected ? width : width - 17,
decoration: BoxDecoration(
color: isSelected
? isDarkMode(context)
? Colors.white12
: Colors.black.withOpacity(0.07)
? _bottomItemOverlayColor.resolve(context)
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(50),
@@ -156,7 +156,11 @@ class _MyHomePageState extends State<MyHomePage>
children: tabItems.map(
(item) {
int itemIndex = tabItems.indexOf(item);
return _buildItem(itemIndex, item, _selectIndex == itemIndex);
return _buildBottomItem(
itemIndex,
item,
_selectIndex == itemIndex,
);
},
).toList(),
),
@@ -170,8 +174,19 @@ class _MyHomePageState extends State<MyHomePage>
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildIcon(),
const Text(BuildData.name),
Text(_versionStr),
TextButton(
onPressed: () => showRoundDialog(
context,
_versionStr,
const Text(BuildData.buildAt),
[],
),
child: Text(
'${BuildData.name}\n$_versionStr',
textAlign: TextAlign.center,
style: textSize13,
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.07,
),
@@ -189,8 +204,9 @@ class _MyHomePageState extends State<MyHomePage>
leading: const Icon(Icons.vpn_key),
title: Text(_s.privateKey),
onTap: () => AppRoute(
const StoredPrivateKeysPage(), 'private key list')
.go(context),
const PrivateKeysListPage(),
'private key list',
).go(context),
),
ListTile(
leading: const Icon(Icons.download),
@@ -213,11 +229,6 @@ class _MyHomePageState extends State<MyHomePage>
_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),
@@ -237,7 +248,7 @@ class _MyHomePageState extends State<MyHomePage>
),
AboutListTile(
icon: const Icon(Icons.text_snippet),
applicationName: BuildData.name,
applicationName: '\n${BuildData.name}',
applicationVersion: _versionStr,
applicationIcon: _buildIcon(),
aboutBoxChildren: [
@@ -247,16 +258,15 @@ class _MyHomePageState extends State<MyHomePage>
UrlText(
text: _s.aboutThanks,
),
const UrlText(
text: rainSunMeGithub,
replace: 'RainSunMe',
),
const UrlText(
text: fectureGithub,
replace: 'fecture',
// Thanks
...thanksMap.keys.map(
(key) => UrlText(
text: thanksMap[key] ?? '',
replace: key,
),
)
],
child: Text(_s.license),
child: Text(_s.about),
)
],
),
@@ -273,7 +283,7 @@ class _MyHomePageState extends State<MyHomePage>
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 53, maxWidth: 53),
child: Container(
color: primaryColor,
color: Colors.white,
),
),
ConstrainedBox(

View File

@@ -1,14 +1,18 @@
import 'dart:async';
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/ping_result.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../core/extension/uint8list.dart';
import '../../core/utils/ui.dart';
import '../../data/model/server/ping_result.dart';
import '../../data/provider/server.dart';
import '../../data/res/color.dart';
import '../../data/res/font_style.dart';
import '../../locator.dart';
import '../widget/input_field.dart';
import '../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,}$');
@@ -24,7 +28,7 @@ class PingPage extends StatefulWidget {
}
class _PingPageState extends State<PingPage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late TextEditingController _textEditingController;
late MediaQueryData _media;
final List<PingResult> _results = [];
@@ -41,7 +45,7 @@ class _PingPageState extends State<PingPage>
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
s = S.of(context)!;
}
@override
@@ -53,20 +57,24 @@ class _PingPageState extends State<PingPage>
child: Column(
children: [
const SizedBox(height: 13),
buildInput(context, _textEditingController,
hint: s.inputDomainHere,
maxLines: 1,
onSubmitted: (_) => doPing()),
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);
}),
controller: ScrollController(),
itemCount: _results.length,
itemBuilder: (context, index) {
final result = _results[index];
return _buildResultItem(result);
},
),
),
],
),
@@ -153,7 +161,7 @@ class _PingPageState extends State<PingPage>
return;
}
final result = await e.client!.run('ping -c 3 $target').string;
_results.add(PingResult.parse(e.info.name, result));
_results.add(PingResult.parse(e.spi.name, result));
setState(() {});
}));
} catch (e) {
@@ -163,4 +171,12 @@ class _PingPageState extends State<PingPage>
@override
bool get wantKeepAlive => true;
@override
Future<FutureOr<void>> afterFirstLayout(BuildContext context) async {
if (_serverProvider.servers.isEmpty) {
await _serverProvider.loadLocalData();
await _serverProvider.refreshData();
}
}
}

View File

@@ -1,19 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/pkg/upgrade_info.dart';
import 'package:toolbox/data/model/server/dist.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/pkg.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import 'package:toolbox/view/widget/url_text.dart';
import '../../data/model/pkg/upgrade_info.dart';
import '../../data/model/server/dist.dart';
import '../../core/utils/ui.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/pkg.dart';
import '../../data/provider/server.dart';
import '../../data/res/font_style.dart';
import '../../locator.dart';
import '../widget/center_loading.dart';
import '../widget/round_rect_card.dart';
import '../widget/two_line_text.dart';
class PkgManagePage extends StatefulWidget {
const PkgManagePage(this.spi, {Key? key}) : super(key: key);
@@ -37,7 +36,7 @@ class _PkgManagePageState extends State<PkgManagePage>
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -51,7 +50,7 @@ class _PkgManagePageState extends State<PkgManagePage>
super.initState();
final si = locator<ServerProvider>()
.servers
.firstWhere((e) => e.info == widget.spi);
.firstWhere((e) => e.spi == widget.spi);
if (si.client == null) {
showSnackBar(context, Text(_s.waitConnection));
Navigator.of(context).pop();
@@ -79,7 +78,9 @@ class _PkgManagePageState extends State<PkgManagePage>
Text(_s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
),
],
);
return;
@@ -109,11 +110,12 @@ class _PkgManagePageState extends State<PkgManagePage>
},
child: Text(_s.cancel)),
TextButton(
onPressed: () => onSubmitted(),
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
)),
onPressed: () => onSubmitted(),
child: Text(
_s.ok,
style: const TextStyle(color: Colors.red),
),
),
],
);
return _textController.text.trim();
@@ -124,7 +126,7 @@ class _PkgManagePageState extends State<PkgManagePage>
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: 'Apt', down: widget.spi.name),
title: TwoLineText(up: _s.pkg, down: widget.spi.name),
),
body: Consumer<PkgProvider>(builder: (_, apt, __) {
if (apt.error != null) {
@@ -139,8 +141,10 @@ class _PkgManagePageState extends State<PkgManagePage>
const SizedBox(
height: 37,
),
SizedBox(
height: _media.size.height * 0.4,
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: _media.size.height * 0.3,
minWidth: _media.size.width),
child: Padding(
padding: const EdgeInsets.all(17),
child: RoundRectCard(
@@ -148,7 +152,6 @@ class _PkgManagePageState extends State<PkgManagePage>
padding: const EdgeInsets.all(17),
child: Text(
apt.error!,
textAlign: TextAlign.center,
),
),
),
@@ -176,18 +179,8 @@ class _PkgManagePageState extends State<PkgManagePage>
}
return ListView(
padding: const EdgeInsets.all(13),
children: [
Padding(
padding: const EdgeInsets.all(17),
child: UrlText(
text:
'${_s.experimentalFeature}\n${_s.reportBugsOnGithubIssue(issueUrl)}',
replace: 'Github Issue',
textAlign: TextAlign.center,
),
),
_buildUpdatePanel(apt)
].map((e) => RoundRectCard(e)).toList(),
children:
[_buildUpdatePanel(apt)].map((e) => RoundRectCard(e)).toList(),
);
}),
);
@@ -203,49 +196,31 @@ class _PkgManagePageState extends State<PkgManagePage>
subtitle: const Text('>_<', textAlign: TextAlign.center),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ExpansionTile(
title: Text(_s.foundNUpdate(apt.upgradeable!.length)),
subtitle: Text(
apt.upgradeable!.map((e) => e.package).join(', '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: grey,
),
children: apt.upgradeLog == null
? [
TextButton(
child: Text(_s.updateAll),
onPressed: () {
apt.upgrade();
}),
SizedBox(
height: _media.size.height * 0.73,
child: ListView(
controller: _scrollController,
children: apt.upgradeable!
.map((e) => _buildUpdateItem(e, apt))
.toList(),
),
)
]
: [
SizedBox(
height: _media.size.height * 0.7,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(18),
controller: _scrollController,
child: Text(apt.upgradeLog!),
),
),
)
],
)
],
return ExpansionTile(
title: Text(_s.foundNUpdate(apt.upgradeable!.length)),
subtitle: Text(
apt.upgradeable!.map((e) => e.package).join(', '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: grey,
),
children: apt.upgradeLog == null
? [
TextButton(
child: Text(_s.updateAll),
onPressed: () {
apt.upgrade();
},
),
...apt.upgradeable!.map((e) => _buildUpdateItem(e, apt)).toList()
]
: [
SingleChildScrollView(
padding: const EdgeInsets.all(18),
controller: _scrollController,
child: Text(apt.upgradeLog!),
)
],
);
}

View File

@@ -1,15 +1,21 @@
import 'dart:io';
import 'package:after_layout/after_layout.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_decoration.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/data/res/misc.dart';
import '../../../core/utils/server.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/private_key_info.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/res/font_style.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';
const _format = 'text/plain';
@@ -47,7 +53,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
_focusScope = FocusScope.of(context);
}
@@ -89,6 +95,38 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
enableSuggestions: false,
decoration: buildDecoration(_s.privateKey, icon: Icons.vpn_key),
),
TextButton(
onPressed: () async {
final path = await pickOneFile();
if (path == null) {
showSnackBar(context, const Text('path is null'));
return;
}
final file = File(path);
if (!file.existsSync()) {
showSnackBar(context, Text(_s.fileNotExist(path)));
return;
}
final size = (await file.stat()).size;
if (size > privateKeyMaxSize) {
showSnackBar(
context,
Text(
_s.fileTooLarge(
path,
size.convertBytes,
privateKeyMaxSize.convertBytes,
),
),
);
return;
}
_keyController.text = await file.readAsString();
},
child: Text(_s.pickFile),
),
TextField(
controller: _pwdController,
autocorrect: false,
@@ -120,7 +158,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
),
);
});
final info = PrivateKeyInfo(name, key, pwd);
final info = PrivateKeyInfo(name, key, '');
bool haveErr = false;
try {
info.privateKey = await compute(decyptPem, [key, pwd]);
@@ -159,11 +197,3 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
}
}
}
/// [args] : [key, pwd]
String decyptPem(List<String> args) {
/// skip when the key is not encrypted, or will throw exception
if (!SSHKeyPair.isEncryptedPem(args[0])) return args[0];
final sshKey = SSHKeyPair.fromPem(args[0], args[1]);
return sshKey.first.toPem();
}

View File

@@ -1,27 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
class StoredPrivateKeysPage extends StatefulWidget {
const StoredPrivateKeysPage({Key? key}) : super(key: key);
import '../../../core/route.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/res/font_style.dart';
import 'edit.dart';
import '../../../view/widget/round_rect_card.dart';
class PrivateKeysListPage extends StatefulWidget {
const PrivateKeysListPage({Key? key}) : super(key: key);
@override
_PrivateKeyListState createState() => _PrivateKeyListState();
}
class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
class _PrivateKeyListState extends State<PrivateKeysListPage> {
late S _s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -32,36 +32,31 @@ class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
),
body: Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
return key.infos.isNotEmpty
? ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.infos.length,
itemExtent: 57,
itemBuilder: (context, idx) {
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
key.infos[idx].id,
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => AppRoute(
PrivateKeyEditPage(info: key.infos[idx]),
'private key edit page')
.go(context),
child: Text(_s.edit),
)
],
),
));
})
: Center(
child: Text(_s.noSavedPrivateKey),
);
if (key.infos.isEmpty) {
return Center(
child: Text(_s.noSavedPrivateKey),
);
}
return ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.infos.length,
itemBuilder: (context, idx) {
return RoundRectCard(
ListTile(
title: Text(
key.infos[idx].id,
),
trailing: TextButton(
onPressed: () => AppRoute(
PrivateKeyEditPage(info: key.infos[idx]),
'private key edit page',
).go(context),
child: Text(_s.edit),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(

View File

@@ -1,18 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/data/model/server/dist.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../core/extension/numx.dart';
import '../../../data/model/server/dist.dart';
import '../../../data/model/server/net_speed.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_status.dart';
import '../../../data/provider/server.dart';
import '../../../data/res/color.dart';
import '../../../data/res/font_style.dart';
import '../../../data/res/padding.dart';
import '../../../data/res/sizedbox.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/round_rect_card.dart';
class ServerDetailPage extends StatefulWidget {
const ServerDetailPage(this.id, {Key? key}) : super(key: key);
@@ -27,16 +29,14 @@ class _ServerDetailPageState extends State<ServerDetailPage>
with SingleTickerProviderStateMixin {
late MediaQueryData _media;
late S _s;
late Color pColor;
bool _showDistLogo = true;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context);
_s = S.of(context)!;
_showDistLogo = locator<SettingStore>().showDistLogo.fetch()!;
pColor = primaryColor;
}
@override
@@ -44,16 +44,16 @@ class _ServerDetailPageState extends State<ServerDetailPage>
return Consumer<ServerProvider>(builder: (_, provider, __) {
return _buildMainPage(
provider.servers.firstWhere(
(e) => '${e.info.ip}:${e.info.port}' == widget.id,
(e) => e.spi.id == widget.id,
),
);
});
}
Widget _buildMainPage(ServerInfo si) {
Widget _buildMainPage(Server si) {
return Scaffold(
appBar: AppBar(
title: Text(si.info.name, style: textSize18),
title: Text(si.spi.name, style: textSize18),
),
body: ListView(
padding: const EdgeInsets.all(13),
@@ -62,6 +62,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
_buildUpTimeAndSys(si.status),
_buildCPUView(si.status),
_buildMemView(si.status),
_buildSwapView(si.status),
_buildDiskView(si.status),
_buildNetView(si.status.netSpeed),
// avoid the hieght of navigation bar
@@ -92,45 +93,46 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildCPUView(ServerStatus ss) {
final tempWidget = ss.cpu.temp.isEmpty
? const SizedBox()
: Text(
ss.cpu.temp,
style: textSize13Grey,
);
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: SizedBox(
height: 12 * ss.cpu2Status.coresCount + 63,
child: Column(children: [
SizedBox(
height: _media.size.height * 0.02,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%',
style: textSize27,
textScaleFactor: 1.0,
),
Row(
children: [
_buildDetailPercent(ss.cpu2Status.user, 'user'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildDetailPercent(ss.cpu2Status.sys, 'sys'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildDetailPercent(ss.cpu2Status.iowait, 'io'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildDetailPercent(ss.cpu2Status.idle, 'idle')
],
)
],
),
_buildCPUProgress(ss)
]),
),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'${ss.cpu.usedPercent(coreIdx: 0).toInt()}%',
style: textSize27,
textScaleFactor: 1.0,
),
width7,
tempWidget
],
),
Row(
children: [
_buildDetailPercent(ss.cpu.user, 'user'),
width13,
_buildDetailPercent(ss.cpu.sys, 'sys'),
width13,
_buildDetailPercent(ss.cpu.iowait, 'io'),
width13,
_buildDetailPercent(ss.cpu.idle, 'idle')
],
)
],
),
height13,
_buildCPUProgress(ss)
]),
),
);
}
@@ -156,21 +158,17 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildCPUProgress(ServerStatus ss) {
return SizedBox(
height: 12.0 * ss.cpu2Status.coresCount,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 13),
itemBuilder: (ctx, idx) {
if (idx == 0) return const SizedBox();
return Padding(
padding: const EdgeInsets.all(2),
child: _buildProgress(ss.cpu2Status.usedPercent(coreIdx: idx)),
);
},
itemCount: ss.cpu2Status.coresCount,
),
);
final children = <Widget>[];
for (var i = 0; i < ss.cpu.coresCount; i++) {
if (i == 0) continue;
children.add(
Padding(
padding: const EdgeInsets.all(2),
child: _buildProgress(ss.cpu.usedPercent(coreIdx: i)),
),
);
}
return Column(children: children);
}
Widget _buildProgress(double percent) {
@@ -180,36 +178,37 @@ class _ServerDetailPageState extends State<ServerDetailPage>
value: percentWithinOne,
minHeight: 7,
backgroundColor: progressColor.resolve(context),
color: pColor.withOpacity(0.5 + percentWithinOne / 2),
color: primaryColor,
);
}
Widget _buildUpTimeAndSys(ServerStatus ss) {
return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(ss.sysVer, style: textSize11, textScaleFactor: 1.0),
Text(
ss.uptime,
style: textSize11,
textScaleFactor: 1.0,
),
],
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(ss.sysVer, style: textSize11, textScaleFactor: 1.0),
Text(
ss.uptime,
style: textSize11,
textScaleFactor: 1.0,
),
],
),
),
));
);
}
Widget _buildMemView(ServerStatus ss) {
final used = ss.memory.used / ss.memory.total * 100;
final free = ss.memory.free / ss.memory.total * 100;
final avail = ss.memory.avail / ss.memory.total * 100;
final used = ss.mem.used / ss.mem.total * 100;
final free = ss.mem.free / ss.mem.total * 100;
final avail = ss.mem.avail / ss.mem.total * 100;
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: SizedBox(
height: 70,
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@@ -220,50 +219,73 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Row(
children: [
Text('${used.toStringAsFixed(0)}%', style: textSize27),
const SizedBox(width: 7),
Text('of ${(ss.memory.total * 1024).convertBytes}',
width7,
Text('of ${(ss.mem.total * 1024).convertBytes}',
style: textSize13Grey)
],
),
Row(
children: [
_buildDetailPercent(free, 'free'),
SizedBox(
width: _media.size.width * 0.03,
),
width13,
_buildDetailPercent(avail, 'avail'),
],
),
],
),
const SizedBox(
height: 11,
),
height13,
_buildProgress(used)
],
),
),
));
);
}
Widget _buildSwapView(ServerStatus ss) {
if (ss.swap.total == 0) return const SizedBox();
final used = ss.swap.used / ss.swap.total * 100;
final cached = ss.swap.cached / ss.swap.total * 100;
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text('${used.toStringAsFixed(0)}%', style: textSize27),
width7,
Text('of ${(ss.swap.total * 1024).convertBytes} ',
style: textSize13Grey)
],
),
_buildDetailPercent(cached, 'cached'),
],
),
height13,
_buildProgress(used)
],
),
),
);
}
Widget _buildDiskView(ServerStatus ss) {
final clone = ss.disk.toList();
for (var item in ss.disk) {
if (ignorePath.any((ele) => item.mountLocation.contains(ele))) {
if (_ignorePath.any((ele) => item.path.startsWith(ele))) {
clone.remove(item);
}
}
return RoundRectCard(SizedBox(
height: 27 * clone.length + 25,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
physics: const NeverScrollableScrollPhysics(),
itemCount: clone.length,
itemBuilder: (_, idx) {
final disk = clone[idx];
return Padding(
padding: const EdgeInsets.all(3),
final children = clone
.map((disk) => Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -273,16 +295,23 @@ class _ServerDetailPageState extends State<ServerDetailPage>
style: textSize11,
textScaleFactor: 1.0,
),
Text(disk.mountPath,
style: textSize11, textScaleFactor: 1.0)
Text(disk.path, style: textSize11, textScaleFactor: 1.0)
],
),
_buildProgress(disk.usedPercent.toDouble())
],
),
);
}),
));
))
.toList();
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
),
);
}
Widget _buildNetView(NetSpeed ns) {
@@ -303,24 +332,23 @@ class _ServerDetailPageState extends State<ServerDetailPage>
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
}
return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
child: Column(
children: children,
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Column(
children: children,
),
),
));
);
}
Widget _buildNetSpeedTop() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
padding: const EdgeInsets.only(bottom: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.device_hub,
size: 17,
),
Icon(Icons.device_hub, size: 17),
Icon(Icons.arrow_downward, size: 17),
Icon(Icons.arrow_upward, size: 17),
],
@@ -329,39 +357,42 @@ class _ServerDetailPageState extends State<ServerDetailPage>
}
Widget _buildNetSpeedItem(NetSpeed ns, String device) {
final width = (_media.size.width - 34 - 34) / 3;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: _media.size.width / 4,
child: Text(device, style: textSize11, textScaleFactor: 1.0)),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedIn(device: device),
style: textSize11,
textAlign: TextAlign.center,
textScaleFactor: 1.0),
width: width,
child: Text(
device,
style: textSize11,
textScaleFactor: 1.0,
),
),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedOut(device: device),
style: textSize11,
textAlign: TextAlign.right,
textScaleFactor: 1.0),
width: width,
child: Text(
'${ns.speedIn(device: device)} | ${ns.totalIn(device: device)}',
style: textSize11,
textAlign: TextAlign.center,
textScaleFactor: 0.9,
),
),
SizedBox(
width: width,
child: Text(
'${ns.speedOut(device: device)} | ${ns.totalOut(device: device)}',
style: textSize11,
textAlign: TextAlign.right,
textScaleFactor: 0.9,
),
)
],
),
);
}
static const ignorePath = [
'/run',
'/sys',
'/dev/shm',
'/snap',
'/var/lib/docker',
'/dev/tty'
];
static const _ignorePath = ['udev', 'tmpfs', 'devtmpfs'];
}

View File

@@ -1,19 +1,20 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.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/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/widget/input_decoration.dart';
import '../../../core/route.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/private_key_info.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/provider/private_key.dart';
import '../../../data/provider/server.dart';
import '../../../data/res/color.dart';
import '../../../data/res/font_style.dart';
import '../../../data/store/private_key.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';
import '../private_key/edit.dart';
class ServerEditPage extends StatefulWidget {
const ServerEditPage({Key? key, this.spi}) : super(key: key);
@@ -52,7 +53,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
_focusScope = FocusScope.of(context);
}
@@ -72,7 +73,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
[
TextButton(
onPressed: () {
_serverProvider.delServer(widget.spi!);
_serverProvider.delServer(widget.spi!.id);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
@@ -104,8 +105,11 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
keyboardType: TextInputType.text,
focusNode: _nameFocus,
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
decoration: buildDecoration(_s.name,
icon: Icons.info, hint: _s.exampleName),
decoration: buildDecoration(
_s.name,
icon: Icons.info,
hint: _s.exampleName,
),
),
TextField(
controller: _ipController,
@@ -114,16 +118,22 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
focusNode: _ipFocus,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration(_s.host,
icon: Icons.storage, hint: 'example.com'),
decoration: buildDecoration(
_s.host,
icon: Icons.storage,
hint: 'example.com',
),
),
TextField(
controller: _portController,
keyboardType: TextInputType.number,
focusNode: _portFocus,
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
decoration: buildDecoration(_s.port,
icon: Icons.format_list_numbered, hint: '22'),
decoration: buildDecoration(
_s.port,
icon: Icons.format_list_numbered,
hint: '22',
),
),
TextField(
controller: _usernameController,
@@ -131,16 +141,20 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
focusNode: _usernameFocus,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration(_s.user,
icon: Icons.account_box, hint: 'root'),
decoration: buildDecoration(
_s.user,
icon: Icons.account_box,
hint: 'root',
),
),
const SizedBox(height: 7),
Row(
children: [
Text(_s.keyAuth),
Switch(
value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val)),
value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val),
),
],
),
!usePublicKey
@@ -148,8 +162,11 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: _passwordController,
obscureText: true,
keyboardType: TextInputType.text,
decoration: buildDecoration(_s.pwd,
icon: Icons.password, hint: _s.pwd),
decoration: buildDecoration(
_s.pwd,
icon: Icons.password,
hint: _s.pwd,
),
onSubmitted: (_) => {},
)
: const SizedBox(),
@@ -164,9 +181,10 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final tiles = key.infos
.map(
(e) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), e)),
contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), e),
),
)
.toList();
tiles.add(
@@ -176,9 +194,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () => AppRoute(
const PrivateKeyEditPage(),
'private key edit page')
.go(context),
const PrivateKeyEditPage(),
'private key edit page',
).go(context),
),
),
);
@@ -187,6 +205,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
iconColor: primaryColor,
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
initiallyExpanded: true,
title: Text(
_s.choosePrivateKey,
style: const TextStyle(fontSize: 14),
@@ -213,11 +232,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
Text(_s.sureNoPwd),
[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(false),
child: Text(_s.ok),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(_s.cancel))
onPressed: () => Navigator.of(context).pop(true),
child: Text(_s.cancel),
)
],
barrierDismiss: false,
);

View File

@@ -1,32 +1,34 @@
import 'package:after_layout/after_layout.dart';
import 'package:circle_chart/circle_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_it/get_it.dart';
import 'package:provider/provider.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_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/pkg.dart';
import 'package:toolbox/view/page/docker.dart';
import 'package:toolbox/view/page/server/detail.dart';
import 'package:toolbox/view/page/server/edit.dart';
import 'package:toolbox/view/page/sftp/view.dart';
import 'package:toolbox/view/page/snippet/edit.dart';
import 'package:toolbox/view/page/ssh.dart';
import 'package:toolbox/view/widget/picker.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../core/route.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/model/server/server_status.dart';
import '../../../data/provider/server.dart';
import '../../../data/provider/snippet.dart';
import '../../../data/res/color.dart';
import '../../../data/res/font_style.dart';
import '../../../data/res/menu.dart';
import '../../../data/res/url.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../../widget/dropdown_menu.dart';
import '../../widget/picker.dart';
import '../../widget/round_rect_card.dart';
import '../../widget/url_text.dart';
import '../docker.dart';
import '../pkg.dart';
import '../sftp/view.dart';
import '../snippet/edit.dart';
import '../ssh.dart';
import 'detail.dart';
import 'edit.dart';
class ServerPage extends StatefulWidget {
const ServerPage({Key? key}) : super(key: key);
@@ -39,14 +41,15 @@ class _ServerPageState extends State<ServerPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late MediaQueryData _media;
late ThemeData _theme;
late Color _primaryColor;
late ServerProvider _serverProvider;
late SettingStore _settingStore;
late S _s;
@override
void initState() {
super.initState();
_serverProvider = locator<ServerProvider>();
_settingStore = locator<SettingStore>();
}
@override
@@ -54,45 +57,49 @@ class _ServerPageState extends State<ServerPage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_primaryColor = primaryColor;
_s = S.of(context);
_s = S.of(context)!;
}
@override
Widget build(BuildContext context) {
super.build(context);
final child = Consumer<ServerProvider>(
builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return Center(
child: Text(
_s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(7),
controller: ScrollController(),
itemBuilder: (ctx, idx) {
if (idx == pro.servers.length) {
return SizedBox(height: _media.padding.bottom);
}
return _buildEachServerCard(pro.servers[idx]);
},
itemCount: pro.servers.length + 1,
separatorBuilder: (_, __) => const SizedBox(
height: 3,
),
);
},
);
return Scaffold(
body: child,
body: RefreshIndicator(
onRefresh: () async =>
await _serverProvider.refreshData(onlyFailed: true),
child: Consumer<ServerProvider>(
builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return Center(
child: Text(
_s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(7),
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (ctx, idx) {
if (idx == pro.servers.length) {
return SizedBox(height: _media.padding.bottom);
}
return _buildEachServerCard(pro.servers[idx]);
},
itemCount: pro.servers.length + 1,
separatorBuilder: (_, __) => const SizedBox(
height: 3,
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
AppRoute(const ServerEditPage(), 'Add server info page')
.go(context),
onPressed: () => AppRoute(
const ServerEditPage(),
'Add server info page',
).go(context),
tooltip: _s.addAServer,
heroTag: 'server page fab',
child: const Icon(Icons.add),
@@ -100,36 +107,32 @@ class _ServerPageState extends State<ServerPage>
);
}
Widget _buildEachServerCard(ServerInfo si) {
Widget _buildEachServerCard(Server si) {
return RoundRectCard(
InkWell(
onLongPress: () => AppRoute(
ServerEditPage(
spi: si.info,
spi: si.spi,
),
'Edit server info page')
.go(context),
child: Padding(
padding: const EdgeInsets.all(13),
child: _buildRealServerCard(
si.status, si.info.name, si.connectionState, si.info),
child: _buildRealServerCard(si.status, si.spi.name, si.cs, si.spi),
),
onTap: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'),
'server detail page')
onTap: () => AppRoute(ServerDetailPage(si.spi.id), 'server detail page')
.go(context),
),
);
}
Widget _buildRealServerCard(ServerStatus ss, String serverName,
ServerConnectionState cs, ServerPrivateInfo spi) {
final rootDisk =
ss.disk.firstWhere((element) => element.mountLocation == '/');
ServerState cs, ServerPrivateInfo spi) {
final rootDisk = ss.disk.firstWhere((element) => element.loc == '/');
final topRightStr =
getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime, ss.failedInfo);
final hasError =
cs == ServerConnectionState.failed && ss.failedInfo != null;
getTopRightStr(cs, ss.cpu.temp, ss.uptime, ss.failedInfo);
final hasError = cs == ServerState.failed && ss.failedInfo != null;
final style = TextStyle(
color: _theme.textTheme.bodyLarge!.color!.withAlpha(100), fontSize: 11);
@@ -162,7 +165,11 @@ class _ServerPageState extends State<ServerPage>
? GestureDetector(
onTap: () => showRoundDialog(
context, _s.error, Text(ss.failedInfo ?? ''), []),
child: Text(_s.clickSee, style: style))
child: Text(
_s.clickSee,
style: style,
textScaleFactor: 1.0,
))
: Text(topRightStr, style: style, textScaleFactor: 1.0),
const SizedBox(width: 9),
_buildSSHBtn(spi),
@@ -178,8 +185,8 @@ class _ServerPageState extends State<ServerPage>
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildPercentCircle(ss.cpu2Status.usedPercent()),
_buildPercentCircle(ss.memory.used / ss.memory.total * 100),
_buildPercentCircle(ss.cpu.usedPercent()),
_buildPercentCircle(ss.mem.used / ss.mem.total * 100),
_buildIOData('Conn:\n${ss.tcp.maxConn}', 'Fail:\n${ss.tcp.fail}'),
_buildIOData(
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
@@ -206,22 +213,30 @@ class _ServerPageState extends State<ServerPage>
Icons.terminal,
size: 21,
),
onTap: () => showRoundDialog(
context,
_s.attention,
UrlText(
text: _s.sshTip(issueUrl),
replace: 'Github Issue',
),
[
TextButton(
onPressed: () {
Navigator.of(context).pop();
AppRoute(SSHPage(spi: spi), 'ssh page').go(context);
},
child: Text(_s.ok),
)
]),
onTap: () async {
if (_settingStore.firstTimeUseSshTerm.fetch()!) {
await showRoundDialog(
context,
_s.attention,
UrlText(
text: _s.sshTip(issueUrl),
replace: 'Github Issue',
),
[
TextButton(
onPressed: () {
Navigator.of(context).pop();
AppRoute(SSHPage(spi: spi), 'ssh page').go(context);
},
child: Text(_s.ok),
)
],
);
_settingStore.firstTimeUseSshTerm.put(false);
} else {
AppRoute(SSHPage(spi: spi), 'ssh page').go(context);
}
},
);
}
@@ -231,14 +246,14 @@ class _ServerPageState extends State<ServerPage>
...ServerTabMenuItems.firstItems.map(
(item) => PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
child: item.build(_s),
),
),
const PopupMenuDivider(height: 1),
...ServerTabMenuItems.secondItems.map(
(item) => PopupMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
child: item.build(_s),
),
),
],
@@ -276,12 +291,12 @@ class _ServerPageState extends State<ServerPage>
);
}
String getTopRightStr(ServerConnectionState cs, String temp, String upTime,
String? failedInfo) {
String getTopRightStr(
ServerState cs, String temp, String upTime, String? failedInfo) {
switch (cs) {
case ServerConnectionState.disconnected:
case ServerState.disconnected:
return _s.disconnected;
case ServerConnectionState.connected:
case ServerState.connected:
if (temp == '') {
if (upTime == '') {
return _s.serverTabLoading;
@@ -295,9 +310,9 @@ class _ServerPageState extends State<ServerPage>
return '$temp | $upTime';
}
}
case ServerConnectionState.connecting:
case ServerState.connecting:
return _s.serverTabConnecting;
case ServerConnectionState.failed:
case ServerState.failed:
if (failedInfo == null) {
return _s.serverTabFailed;
}
@@ -345,7 +360,7 @@ class _ServerPageState extends State<ServerPage>
children: [
Center(
child: CircleChart(
progressColor: _primaryColor,
progressColor: primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 53,
@@ -380,8 +395,10 @@ class _ServerPageState extends State<ServerPage>
child: Text(_s.ok),
),
TextButton(
onPressed: () =>
AppRoute(const SnippetEditPage(), 'edit snippet').go(context),
onPressed: () {
Navigator.of(context).pop();
AppRoute(const SnippetEditPage(), 'edit snippet').go(context);
},
child: Text(_s.addOne),
)
],
@@ -393,22 +410,24 @@ class _ServerPageState extends State<ServerPage>
showRoundDialog(
context,
_s.choose,
buildPicker(provider.snippets.map((e) => Text(e.name)).toList(),
(idx) => snippet = provider.snippets[idx]),
buildPicker(
provider.snippets.map((e) => Text(e.name)).toList(),
(idx) => snippet = provider.snippets[idx],
),
[
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final result =
await locator<ServerProvider>().runSnippet(id, snippet);
final result = await _serverProvider.runSnippet(id, snippet);
showRoundDialog(
context,
_s.result,
Text(result ?? _s.error, style: textSize13),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok))
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
)
],
);
},

View File

@@ -1,19 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/update.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/data/res/tab.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../data/model/ssh/terminal_color.dart';
import '../../core/update.dart';
import '../../core/utils/ui.dart';
import '../../data/provider/app.dart';
import '../../data/provider/server.dart';
import '../../data/res/build_data.dart';
import '../../data/res/color.dart';
import '../../data/res/font_style.dart';
import '../../data/res/tab.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/round_rect_card.dart';
class SettingPage extends StatefulWidget {
const SettingPage({Key? key}) : super(key: key);
@@ -24,23 +25,22 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> {
late final SettingStore _setting;
late int _selectedColorValue;
late int _launchPageIdx;
late Color priColor;
late final ServerProvider _serverProvider;
late MediaQueryData _media;
late ThemeData _theme;
late S _s;
var _updateInterval = 5.0;
late int _selectedColorValue;
late int _launchPageIdx;
late int _termThemeIdx;
late int _nightMode;
late double _maxRetryCount;
late double _updateInterval;
@override
void didChangeDependencies() {
super.didChangeDependencies();
priColor = primaryColor;
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -49,7 +49,10 @@ class _SettingPageState extends State<SettingPage> {
_serverProvider = locator<ServerProvider>();
_setting = locator<SettingStore>();
_launchPageIdx = _setting.launchPage.fetch()!;
_termThemeIdx = _setting.termColorIdx.fetch()!;
_nightMode = _setting.nightMode.fetch()!;
_updateInterval = _setting.serverStatusUpdateInterval.fetch()!.toDouble();
_maxRetryCount = _setting.maxRetryCount.fetch()!.toDouble();
}
@override
@@ -59,18 +62,53 @@ class _SettingPageState extends State<SettingPage> {
title: Text(_s.setting),
),
body: ListView(
padding: const EdgeInsets.all(17),
padding: const EdgeInsets.symmetric(horizontal: 17),
children: [
_buildAppColorPreview(),
_buildUpdateInterval(),
_buildCheckUpdate(),
_buildLaunchPage(),
_buildDistLogoSwitch(),
].map((e) => RoundRectCard(e)).toList(),
// App
_buildTitle('App'),
_buildApp(),
// Server
_buildTitle(_s.server),
_buildServer(),
],
),
);
}
Widget _buildTitle(String text) {
return Padding(
padding: const EdgeInsets.only(top: 23, bottom: 17),
child: Center(
child: Text(
text,
style: textSize13,
),
),
);
}
Widget _buildApp() {
return Column(
children: [
_buildNightMode(),
_buildAppColorPreview(),
_buildLaunchPage(),
_buildCheckUpdate(),
].map((e) => RoundRectCard(e)).toList(),
);
}
Widget _buildServer() {
return Column(
children: [
_buildDistLogoSwitch(),
_buildUpdateInterval(),
_buildTermTheme(),
_buildMaxRetry(),
].map((e) => RoundRectCard(e)).toList(),
);
}
Widget _buildDistLogoSwitch() {
return ListTile(
title: Text(
@@ -99,7 +137,6 @@ class _SettingPageState extends State<SettingPage> {
display = _s.versionUnknownUpdate(BuildData.build);
}
return ListTile(
contentPadding: roundRectCardPadding,
trailing: const Icon(Icons.keyboard_arrow_right),
title: Text(
display,
@@ -114,9 +151,7 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildUpdateInterval() {
return ExpansionTile(
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
textColor: priColor,
textColor: primaryColor,
title: Text(
_s.updateServerStatusInterval,
style: textSize13,
@@ -126,11 +161,14 @@ class _SettingPageState extends State<SettingPage> {
_s.willTakEeffectImmediately,
style: textSize13Grey,
),
trailing: Text('${_updateInterval.toInt()} ${_s.second}'),
trailing: Text(
'${_updateInterval.toInt()} ${_s.second}',
style: textSize13,
),
children: [
Slider(
thumbColor: priColor,
activeColor: priColor.withOpacity(0.7),
thumbColor: primaryColor,
activeColor: primaryColor.withOpacity(0.7),
min: 0,
max: 10,
value: _updateInterval,
@@ -165,12 +203,10 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildAppColorPreview() {
return ExpansionTile(
textColor: priColor,
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
textColor: primaryColor,
trailing: ClipOval(
child: Container(
color: priColor,
color: primaryColor,
height: 27,
width: 27,
),
@@ -179,17 +215,18 @@ class _SettingPageState extends State<SettingPage> {
_s.appPrimaryColor,
style: textSize13,
),
children: [_buildAppColorPicker(priColor), _buildColorPickerConfirmBtn()],
children: [_buildAppColorPicker(), _buildColorPickerConfirmBtn()],
);
}
Widget _buildAppColorPicker(Color selected) {
Widget _buildAppColorPicker() {
return MaterialColorPicker(
shrinkWrap: true,
onColorChange: (Color color) {
_selectedColorValue = color.value;
},
selectedColor: selected);
shrinkWrap: true,
onColorChange: (Color color) {
_selectedColorValue = color.value;
},
selectedColor: primaryColor,
);
}
Widget _buildColorPickerConfirmBtn() {
@@ -204,9 +241,8 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildLaunchPage() {
return ExpansionTile(
textColor: priColor,
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
childrenPadding: const EdgeInsets.only(left: 17, right: 7),
textColor: primaryColor,
title: Text(
_s.launchPage,
style: textSize13,
@@ -225,9 +261,7 @@ class _SettingPageState extends State<SettingPage> {
contentPadding: EdgeInsets.zero,
title: Text(
tabTitleName(context, tabs.indexOf(e)),
style: TextStyle(
fontSize: 14,
color: _theme.textTheme.bodyMedium!.color!.withAlpha(177)),
style: textSize13,
),
trailing: _buildRadio(tabs.indexOf(e)),
),
@@ -248,4 +282,141 @@ class _SettingPageState extends State<SettingPage> {
},
);
}
Widget _buildTermTheme() {
return ExpansionTile(
textColor: primaryColor,
childrenPadding: const EdgeInsets.only(left: 17),
title: Text(
_s.termTheme,
style: textSize13,
),
trailing: Text(
TerminalColorsPlatform.values[_termThemeIdx].name,
style: textSize13,
),
children: _buildTermThemeRadioList(),
);
}
List<Widget> _buildTermThemeRadioList() {
return TerminalColorsPlatform.values
.map(
(e) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
e.name,
style: textSize13,
),
trailing: _buildTermThemeRadio(e),
),
)
.toList();
}
Radio _buildTermThemeRadio(TerminalColorsPlatform platform) {
return Radio<int>(
value: platform.index,
groupValue: _termThemeIdx,
onChanged: (int? value) {
setState(() {
value ??= 0;
_termThemeIdx = value!;
_setting.termColorIdx.put(value!);
});
},
);
}
Widget _buildMaxRetry() {
return ExpansionTile(
textColor: primaryColor,
title: Text(
_s.maxRetryCount,
style: textSize13,
textAlign: TextAlign.start,
),
trailing: Text(
'${_maxRetryCount.toInt()} ${_s.times}',
style: textSize13,
),
children: [
Slider(
thumbColor: primaryColor,
activeColor: primaryColor.withOpacity(0.7),
min: 0,
max: 10,
value: _maxRetryCount,
onChanged: (newValue) {
setState(() {
_maxRetryCount = newValue;
});
},
onChangeEnd: (val) {
_setting.maxRetryCount.put(val.toInt());
},
label: '${_maxRetryCount.toInt()} ${_s.times}',
divisions: 10,
),
const SizedBox(
height: 3,
),
_maxRetryCount == 0.0
? Text(
_s.maxRetryCountEqual0,
style: const TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
)
: const SizedBox(),
const SizedBox(
height: 13,
)
],
);
}
Widget _buildNightMode() {
return ExpansionTile(
textColor: primaryColor,
title: Text(
_s.themeMode,
style: textSize13,
textAlign: TextAlign.start,
),
trailing: Text(
_buildNightModeStr(_nightMode),
style: textSize13,
),
children: [
Slider(
thumbColor: primaryColor,
activeColor: primaryColor.withOpacity(0.7),
min: 0,
max: 2,
value: _nightMode.toDouble(),
onChanged: (newValue) {
setState(() {
_nightMode = newValue.toInt();
});
},
onChangeEnd: (val) {
_setting.nightMode.put(val.toInt());
},
label: _buildNightModeStr(_nightMode),
divisions: 2,
),
],
);
}
String _buildNightModeStr(int n) {
switch (n) {
case 1:
return _s.light;
case 2:
return _s.dark;
default:
return _s.auto;
}
}
}

View File

@@ -1,16 +1,18 @@
import 'dart:io';
import 'package:flutter/material.dart';
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/app/path_with_prefix.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/path.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/page/sftp/downloading.dart';
import 'package:toolbox/view/widget/fade_in.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../core/extension/numx.dart';
import '../../../core/extension/stringx.dart';
import '../../../core/route.dart';
import '../../../core/utils/misc.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/app/path_with_prefix.dart';
import '../../../data/res/font_style.dart';
import '../../../data/res/path.dart';
import '../../widget/fade_in.dart';
import 'downloading.dart';
class SFTPDownloadedPage extends StatefulWidget {
const SFTPDownloadedPage({Key? key}) : super(key: key);
@@ -37,7 +39,7 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -149,8 +151,9 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
const SizedBox(),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () {
file.deleteSync();
@@ -174,8 +177,9 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
),
[
TextButton(
onPressed: (() => Navigator.of(context).pop()),
child: Text(_s.close))
onPressed: (() => Navigator.of(context).pop()),
child: Text(_s.close),
)
],
);
}

View File

@@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../core/extension/numx.dart';
import '../../../core/utils/misc.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/sftp/download_status.dart';
import '../../../data/provider/sftp_download.dart';
import '../../../data/res/font_style.dart';
import '../../widget/center_loading.dart';
import '../../widget/round_rect_card.dart';
class SFTPDownloadingPage extends StatefulWidget {
const SFTPDownloadingPage({Key? key}) : super(key: key);
@@ -22,7 +24,7 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -75,6 +77,7 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
Widget _buildItem(SftpDownloadStatus status) {
if (status.error != null) {
showSnackBar(context, Text(status.error.toString()));
status.error = null;
}
switch (status.status) {
case SftpWorkerStatus.finished:

View File

@@ -2,24 +2,25 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart';
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.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';
import 'package:toolbox/data/model/sftp/browser_status.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/res/path.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/sftp/downloading.dart';
import 'package:toolbox/view/widget/fade_in.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../core/extension/numx.dart';
import '../../../core/extension/stringx.dart';
import '../../../core/route.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart';
import '../../../data/model/sftp/absolute_path.dart';
import '../../../data/model/sftp/browser_status.dart';
import '../../../data/model/sftp/download_item.dart';
import '../../../data/provider/server.dart';
import '../../../data/provider/sftp_download.dart';
import '../../../data/res/path.dart';
import '../../../data/store/private_key.dart';
import '../../../locator.dart';
import '../../widget/fade_in.dart';
import '../../widget/two_line_text.dart';
import 'downloading.dart';
class SFTPPage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -36,21 +37,21 @@ class _SFTPPageState extends State<SFTPPage> {
late MediaQueryData _media;
late S _s;
ServerInfo? _si;
Server? _si;
SSHClient? _client;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_s = S.of(context);
_s = S.of(context)!;
}
@override
void initState() {
super.initState();
final serverProvider = locator<ServerProvider>();
_si = serverProvider.servers.firstWhere((s) => s.info == widget.spi);
_si = serverProvider.servers.firstWhere((s) => s.spi == widget.spi);
_client = _si?.client;
}
@@ -62,38 +63,19 @@ class _SFTPPageState extends State<SFTPPage> {
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
actions: [
IconButton(
onPressed: (() => showRoundDialog(
context,
_s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.folder),
title: Text(_s.createFolder),
onTap: () => mkdir(context)),
ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(_s.createFile),
onTap: () => newFile(context)),
],
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.close))
],
)),
icon: const Icon(Icons.add),
)
icon: const Icon(Icons.downloading),
onPressed: () =>
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
.go(context),
),
],
),
body: _buildFileView(),
bottomNavigationBar: _buildPath(),
bottomNavigationBar: SafeArea(child: _buildBottom()),
);
}
Widget _buildPath() {
Widget _buildBottom() {
return SafeArea(
child: Container(
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
@@ -112,6 +94,32 @@ class _SFTPPageState extends State<SFTPPage> {
},
icon: const Icon(Icons.arrow_back),
),
IconButton(
onPressed: (() => showRoundDialog(
context,
_s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.folder),
title: Text(_s.createFolder),
onTap: () => mkdir(context)),
ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(_s.createFile),
onTap: () => newFile(context)),
],
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.close),
)
],
)),
icon: const Icon(Icons.add),
),
IconButton(
padding: const EdgeInsets.all(0),
onPressed: () async {
@@ -133,8 +141,9 @@ class _SFTPPageState extends State<SFTPPage> {
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel))
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
)
],
);
@@ -168,8 +177,7 @@ class _SFTPPageState extends State<SFTPPage> {
);
Widget _buildFileView() {
if (_client == null ||
_si?.connectionState != ServerConnectionState.connected) {
if (_client == null || _si?.cs != ServerState.connected) {
return centerCircleLoading;
}
@@ -271,8 +279,9 @@ class _SFTPPageState extends State<SFTPPage> {
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel))
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
)
],
);
}
@@ -284,8 +293,9 @@ class _SFTPPageState extends State<SFTPPage> {
Text('${_s.dl2Local(name.filename)}\n${_s.keepForeground}'),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
@@ -307,25 +317,6 @@ class _SFTPPageState extends State<SFTPPage> {
);
Navigator.of(context).pop();
showRoundDialog(
context,
_s.goSftpDlPage,
const SizedBox(),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
.go(context);
},
child: Text(_s.ok),
)
],
);
},
child: Text(_s.download),
)
@@ -341,8 +332,9 @@ class _SFTPPageState extends State<SFTPPage> {
Text(_s.sureDelete(file.filename)),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
_status.client!.remove(file.filename);
@@ -384,8 +376,9 @@ class _SFTPPageState extends State<SFTPPage> {
Text(_s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok)),
onPressed: () => Navigator.of(context).pop(),
child: Text(_s.ok),
),
],
);
return;

View File

@@ -1,12 +1,13 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_decoration.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../core/utils/ui.dart';
import '../../../data/model/server/snippet.dart';
import '../../../data/provider/snippet.dart';
import '../../../data/res/font_style.dart';
import '../../../locator.dart';
import '../../widget/input_decoration.dart';
class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
@@ -35,7 +36,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override

View File

@@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/page/snippet/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '/core/route.dart';
import '/data/provider/snippet.dart';
import '/data/res/font_style.dart';
import 'edit.dart';
import '/view/widget/round_rect_card.dart';
class SnippetListPage extends StatefulWidget {
const SnippetListPage({Key? key}) : super(key: key);
@@ -17,14 +16,12 @@ class SnippetListPage extends StatefulWidget {
}
class _SnippetListPageState extends State<SnippetListPage> {
final _textStyle = TextStyle(color: primaryColor);
late S _s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context);
_s = S.of(context)!;
}
@override
@@ -54,30 +51,18 @@ class _SnippetListPageState extends State<SnippetListPage> {
return ListView.builder(
padding: const EdgeInsets.all(13),
itemCount: key.snippets.length,
itemExtent: 57,
itemBuilder: (context, idx) {
return RoundRectCard(
Padding(
padding: roundRectCardPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
key.snippets[idx].name,
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => AppRoute(
SnippetEditPage(snippet: key.snippets[idx]),
'snippet edit page')
.go(context),
child: Text(
_s.edit,
style: _textStyle,
),
),
],
ListTile(
title: Text(
key.snippets[idx].name,
),
trailing: TextButton(
onPressed: () => AppRoute(
SnippetEditPage(snippet: key.snippets[idx]),
'snippet edit page')
.go(context),
child: Text(_s.edit),
),
),
);

View File

@@ -1,17 +1,26 @@
import 'dart:convert';
import 'dart:io';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:xterm/xterm.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart';
import 'package:xterm/xterm.dart' hide TerminalColors;
import '../../core/utils.dart';
import '../../data/model/ssh/terminal_color.dart';
import '../../core/utils/misc.dart';
import '../../core/utils/ui.dart';
import '../../core/utils/server.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/server.dart';
import '../../data/model/ssh/virtual_key.dart';
import '../../data/provider/virtual_keyboard.dart';
import '../../data/res/color.dart';
import '../../data/res/terminal_theme.dart';
import '../../data/res/virtual_key.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/virtual_keyboard.dart';
class SSHPage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -22,98 +31,299 @@ class SSHPage extends StatefulWidget {
}
class _SSHPageState extends State<SSHPage> {
late final terminal = Terminal(inputHandler: keyboard);
late final SSHSession session;
final keyboard = VirtualKeyboard(defaultInputHandler);
late final _terminal = Terminal(inputHandler: _keyboard);
SSHClient? _client;
final _keyboard = locator<VirtualKeyboard>();
late MediaQueryData _media;
final _virtualKeyboardHeight = 57.0;
final TerminalController _terminalController = TerminalController();
final ContextMenuController _menuController = ContextMenuController();
late TextStyle _menuTextStyle;
late TerminalColors _termColors;
late S _s;
var title = '';
var isDark = false;
var _isDark = false;
@override
void initState() {
super.initState();
final termColorIdx = locator<SettingStore>().termColorIdx.fetch()!;
_termColors = TerminalColorsPlatform.values[termColorIdx].colors;
initTerminal();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
isDark = isDarkMode(context);
_isDark = isDarkMode(context);
_media = MediaQuery.of(context);
_menuTextStyle = TextStyle(color: contentColor.resolve(context));
_s = S.of(context)!;
}
@override
void dispose() {
session.close();
_client?.close();
super.dispose();
}
Future<void> initTerminal() async {
terminal.write('Connecting...\r\n');
_terminal.write('Connecting...\r\n');
final client = locator<ServerProvider>()
.servers
.where((e) => e.info.id == widget.spi.id)
.first
.client;
if (client == null) {
terminal.write('Failed to connect\r\n');
return;
}
_client = await genClient(widget.spi);
_terminal.write('Connected\r\n');
terminal.write('Connected\r\n');
session = await client.shell(
final session = await _client!.shell(
pty: SSHPtyConfig(
width: terminal.viewWidth,
height: terminal.viewHeight,
width: _terminal.viewWidth,
height: _terminal.viewHeight,
),
);
terminal.buffer.clear();
terminal.buffer.setCursor(0, 0);
_terminal.buffer.clear();
_terminal.buffer.setCursor(0, 0);
terminal.onTitleChange = (title) {
setState(() => this.title = title);
};
terminal.onResize = (width, height, pixelWidth, pixelHeight) {
session.resizeTerminal(width, height, pixelWidth, pixelHeight);
};
terminal.onOutput = (data) {
_terminal.onOutput = (data) {
session.write(utf8.encode(data) as Uint8List);
};
session.stdout
.cast<List<int>>()
.transform(const Utf8Decoder())
.listen(terminal.write);
_listen(session.stdout);
_listen(session.stderr);
session.stderr
await session.done;
if (mounted) {
Navigator.of(context).pop();
}
}
void _listen(Stream<Uint8List> stream) {
stream
.cast<List<int>>()
.transform(const Utf8Decoder())
.listen(terminal.write);
.listen(_terminal.write);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title, style: textSize18),
),
body: Column(
children: [
Expanded(
child: TerminalView(
terminal,
keyboardType: TextInputType.visiblePassword,
theme: isDark ? termDarkTheme : termLightTheme,
keyboardAppearance: isDark ? Brightness.dark : Brightness.light,
),
),
VirtualKeyboardView(keyboard),
],
final termTheme = _isDark ? termDarkTheme : termLightTheme;
Widget child = Scaffold(
backgroundColor: termTheme.background,
body: _buildBody(termTheme.toTerminalTheme(_termColors)),
bottomNavigationBar: _buildBottom(termTheme.background),
);
if (Platform.isIOS) {
child = AnnotatedRegion(
value: _isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,
child: child,
);
}
return child;
}
Widget _buildBody(TerminalTheme termTheme) {
return SizedBox(
height: _media.size.height -
_virtualKeyboardHeight -
_media.padding.bottom -
_media.padding.top,
child: TerminalView(
_terminal,
controller: _terminalController,
keyboardType: TextInputType.visiblePassword,
theme: termTheme,
deleteDetection: Platform.isIOS,
onTapUp: _onTapUp,
autoFocus: true,
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
),
);
}
Widget _buildBottom(Color bgColor) {
return SafeArea(
child: AnimatedPadding(
padding: _media.viewInsets,
duration: const Duration(milliseconds: 23),
curve: Curves.fastOutSlowIn,
child: Container(
color: bgColor,
height: _virtualKeyboardHeight,
child: Consumer<VirtualKeyboard>(
builder: (_, __, ___) => _buildVirtualKey(),
),
),
),
);
}
Widget _buildVirtualKey() {
final half = virtualKeys.length ~/ 2;
final top = virtualKeys.sublist(0, half);
final bottom = virtualKeys.sublist(half);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: top.map((e) => _buildVirtualKeyItem(e)).toList(),
),
Row(
children: bottom.map((e) => _buildVirtualKeyItem(e)).toList(),
)
],
);
}
Widget _buildVirtualKeyItem(VirtualKey item) {
var selected = false;
switch (item.key) {
case TerminalKey.control:
selected = _keyboard.ctrl;
break;
case TerminalKey.alt:
selected = _keyboard.alt;
break;
default:
break;
}
final child = item.icon != null
? Icon(
item.icon,
color: _isDark ? Colors.white : Colors.black,
size: 17,
)
: Text(
item.text,
style: TextStyle(
color: selected ? primaryColor : null,
fontSize: 17,
),
);
return InkWell(
onTap: () => _doVirtualKey(item),
child: SizedBox(
width: _media.size.width / (virtualKeys.length / 2),
height: _virtualKeyboardHeight / 2,
child: Center(
child: child,
),
),
);
}
void _doVirtualKey(VirtualKey item) {
if (item.func != null) {
_doVirtualKeyFunc(item.func!);
return;
}
if (item.key != null) {
_doVirtualKeyInput(item.key!);
}
}
void _doVirtualKeyInput(TerminalKey key) {
switch (key) {
case TerminalKey.control:
_keyboard.ctrl = !_keyboard.ctrl;
setState(() {});
break;
case TerminalKey.alt:
_keyboard.alt = !_keyboard.alt;
setState(() {});
break;
default:
_terminal.keyInput(key);
break;
}
}
void _doVirtualKeyFunc(VirtualKeyFunc type) {
switch (type) {
case VirtualKeyFunc.toggleIME:
FocusScope.of(context).requestFocus(FocusNode());
break;
case VirtualKeyFunc.backspace:
_terminal.keyInput(TerminalKey.backspace);
break;
case VirtualKeyFunc.paste:
_paste();
break;
case VirtualKeyFunc.copy:
copy(terminalSelected);
break;
}
}
void _paste() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
if (value != null) {
_terminal.textInput(value.text!);
}
});
}
String get terminalSelected {
final range = _terminalController.selection;
if (range == null) {
return '';
}
return _terminal.buffer.getText(range);
}
void _onTapUp(TapUpDetails details, CellOffset offset) {
{
if (_menuController.isShown) {
_menuController.remove();
return;
}
final selected = terminalSelected;
if (selected.trim().isEmpty) {
// _menuController.show(
// context: context,
// contextMenuBuilder: (context) {
// return TextSelectionToolbar(
// anchorAbove: detail.globalPosition,
// anchorBelow: detail.globalPosition,
// children: [
// TextButton(
// child: Text(
// 'Paste',
// style: _menuTextStyle,
// ),
// onPressed: () async {
// _paste();
// _menuController.remove();
// },
// )
// ],
// );
// },
// );
return;
}
_menuController.show(
context: context,
contextMenuBuilder: (context) {
return TextSelectionToolbar(
anchorAbove: details.globalPosition,
anchorBelow: details.globalPosition,
children: [
TextButton(
child: Text(
_s.copy,
style: _menuTextStyle,
),
onPressed: () {
_terminalController.setSelection(null);
copy(selected);
_menuController.remove();
},
),
],
);
},
);
}
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../data/res/menu.dart';
class DropdownBtnItem {
final String text;
final IconData icon;
const DropdownBtnItem({
required this.text,
required this.icon,
});
Widget build(S s) => Row(
children: [
Icon(icon),
const SizedBox(
width: 10,
),
Text(
getDropdownBtnText(s, text),
),
],
);
}

View File

@@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
import '../../data/res/color.dart';
InputDecoration buildDecoration(String label,
{TextStyle? textStyle, IconData? icon, String? hint}) {
return InputDecoration(
labelText: label,
labelStyle: textStyle,
hintText: hint,
icon: Icon(
icon,
color: primaryColor,
));
labelText: label,
labelStyle: textStyle,
hintText: hint,
icon: Icon(
icon,
color: primaryColor,
),
);
}

View File

@@ -1,9 +1,13 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/res/color.dart';
const regUrl =
r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]*";
import '../../core/utils/ui.dart';
final _reg = RegExp(
r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]*");
const _textStyle = TextStyle();
class UrlText extends StatelessWidget {
final String text;
@@ -11,18 +15,17 @@ class UrlText extends StatelessWidget {
final TextAlign? textAlign;
final TextStyle style;
const UrlText(
{Key? key,
required this.text,
this.replace,
this.textAlign,
this.style = const TextStyle()})
: super(key: key);
const UrlText({
Key? key,
required this.text,
this.replace,
this.textAlign,
this.style = _textStyle,
}) : super(key: key);
List<InlineSpan> _getTextSpans(bool isDarkMode) {
List<InlineSpan> _getTextSpans(Color c) {
List<InlineSpan> widgets = <InlineSpan>[];
final reg = RegExp(regUrl);
Iterable<Match> matches = reg.allMatches(text);
Iterable<Match> matches = _reg.allMatches(text);
List<_ResultMatch> resultMatches = <_ResultMatch>[];
int start = 0;
@@ -53,16 +56,22 @@ class UrlText extends StatelessWidget {
for (var result in resultMatches) {
if (result.isUrl) {
widgets.add(_LinkTextSpan(
widgets.add(
_LinkTextSpan(
replace: replace ?? result.text,
text: result.text,
style: style.copyWith(color: Colors.blue)));
style: style.copyWith(color: primaryColor),
),
);
} else {
widgets.add(TextSpan(
widgets.add(
TextSpan(
text: result.text,
style: style.copyWith(
color: isDarkMode ? Colors.white : Colors.black,
)));
color: c,
),
),
);
}
}
return widgets;
@@ -72,7 +81,7 @@ class UrlText extends StatelessWidget {
Widget build(BuildContext context) {
return RichText(
textAlign: textAlign ?? TextAlign.start,
text: TextSpan(children: _getTextSpans(isDarkMode(context))),
text: TextSpan(children: _getTextSpans(contentColor.resolve(context))),
);
}
}
@@ -80,12 +89,13 @@ class UrlText extends StatelessWidget {
class _LinkTextSpan extends TextSpan {
_LinkTextSpan({TextStyle? style, required String text, String? replace})
: super(
style: style,
text: replace,
recognizer: TapGestureRecognizer()
..onTap = () {
openUrl(text);
});
style: style,
text: replace,
recognizer: TapGestureRecognizer()
..onTap = () {
openUrl(text);
},
);
}
class _ResultMatch {

View File

@@ -1,81 +0,0 @@
import 'package:flutter/material.dart';
import 'package:xterm/xterm.dart';
class VirtualKeyboardView extends StatelessWidget {
const VirtualKeyboardView(this.keyboard, {super.key});
final VirtualKeyboard keyboard;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: keyboard,
builder: (context, child) => ToggleButtons(
renderBorder: false,
isSelected: [keyboard.ctrl, keyboard.alt, keyboard.shift],
onPressed: (index) {
switch (index) {
case 0:
keyboard.ctrl = !keyboard.ctrl;
break;
case 1:
keyboard.alt = !keyboard.alt;
break;
case 2:
keyboard.shift = !keyboard.shift;
break;
}
},
children: const [Text('Ctrl'), Text('Alt'), Text('Shift')],
),
);
}
}
class VirtualKeyboard extends TerminalInputHandler with ChangeNotifier {
final TerminalInputHandler _inputHandler;
VirtualKeyboard(this._inputHandler);
bool _ctrl = false;
bool get ctrl => _ctrl;
set ctrl(bool value) {
if (_ctrl != value) {
_ctrl = value;
notifyListeners();
}
}
bool _shift = false;
bool get shift => _shift;
set shift(bool value) {
if (_shift != value) {
_shift = value;
notifyListeners();
}
}
bool _alt = false;
bool get alt => _alt;
set alt(bool value) {
if (_alt != value) {
_alt = value;
notifyListeners();
}
}
@override
String? call(TerminalKeyboardEvent event) {
return _inputHandler.call(event.copyWith(
ctrl: event.ctrl || _ctrl,
shift: event.shift || _shift,
alt: event.alt || _alt,
));
}
}

View File

@@ -26,9 +26,9 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2
url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7

View File

@@ -26,7 +26,7 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
E3D733E2A8794200D26EFCCF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ADDB81DD1CCC4A9ED73177B /* Pods_Runner.framework */; };
CC3C9C24336DBCBB13B1DD4E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051B6E43AB66C836E65690B6 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -53,6 +53,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
051B6E43AB66C836E65690B6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1B64E26251C2132C1FD3DE5F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* server_box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = server_box.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -67,12 +69,10 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
6B35024F2A8A5A7961F90167 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
6F4CBCB4E20C50200E1C67AD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7ADDB81DD1CCC4A9ED73177B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3EBAA16736A04B8FA45DF33F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
A3006D048053A6426855B015 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
E8FDB0F1B04D4A1C2983795D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -80,7 +80,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E3D733E2A8794200D26EFCCF /* Pods_Runner.framework in Frameworks */,
CC3C9C24336DBCBB13B1DD4E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -105,7 +105,7 @@
33CEB47122A05771004F2AC0 /* Flutter */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
B9C356C33EBFDC4109524AEE /* Pods */,
51D4AEDD40A46AA3AF8AF7AE /* Pods */,
);
sourceTree = "<group>";
};
@@ -152,12 +152,12 @@
path = Runner;
sourceTree = "<group>";
};
B9C356C33EBFDC4109524AEE /* Pods */ = {
51D4AEDD40A46AA3AF8AF7AE /* Pods */ = {
isa = PBXGroup;
children = (
6B35024F2A8A5A7961F90167 /* Pods-Runner.debug.xcconfig */,
A3006D048053A6426855B015 /* Pods-Runner.release.xcconfig */,
6F4CBCB4E20C50200E1C67AD /* Pods-Runner.profile.xcconfig */,
1B64E26251C2132C1FD3DE5F /* Pods-Runner.debug.xcconfig */,
E8FDB0F1B04D4A1C2983795D /* Pods-Runner.release.xcconfig */,
3EBAA16736A04B8FA45DF33F /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
@@ -166,7 +166,7 @@
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
7ADDB81DD1CCC4A9ED73177B /* Pods_Runner.framework */,
051B6E43AB66C836E65690B6 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -178,13 +178,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
6380F17CF8505331723EA2D3 /* [CP] Check Pods Manifest.lock */,
33DCC8E03FB9B8627035A7B7 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
3A1CB36BB218FE124CA9BC01 /* [CP] Embed Pods Frameworks */,
DE29B08A1EBC54F140840A8F /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -292,24 +292,7 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
3A1CB36BB218FE124CA9BC01 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
6380F17CF8505331723EA2D3 /* [CP] Check Pods Manifest.lock */ = {
33DCC8E03FB9B8627035A7B7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -331,6 +314,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DE29B08A1EBC54F140840A8F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

@@ -6,16 +6,20 @@ import 'dart:io';
const appName = 'ServerBox';
const buildDataFilePath = 'lib/data/res/build_data.dart';
const apkPath = 'build/app/outputs/flutter-apk/app-release.apk';
const xcarchivePath = 'build/ios/archive/Runner.xcarchive';
var regiOSProjectVer = RegExp(r'CURRENT_PROJECT_VERSION = .+;');
var regiOSMarketVer = RegExp(r'MARKETING_VERSION = .+');
const iOSInfoPlistPath = 'ios/Runner.xcodeproj/project.pbxproj';
const appleXCConfigPath = '/Runner.xcodeproj/project.pbxproj';
var regAppleProjectVer = RegExp(r'CURRENT_PROJECT_VERSION = .+;');
var regAppleMarketVer = RegExp(r'MARKETING_VERSION = .+');
const skslFileSuffix = '.sksl.json';
const buildFuncs = {
'ios': flutterBuildIOS,
'android': flutterBuildAndroid,
'macos': flutterBuildMacOS,
};
int? build;
@@ -24,9 +28,9 @@ Future<ProcessResult> fvmRun(List<String> args) async {
return await Process.run('fvm', args, runInShell: true);
}
Future<int> getGitCommitCount() async {
Future<void> getGitCommitCount() async {
final result = await Process.run('git', ['log', '--oneline']);
return (result.stdout as String)
build = (result.stdout as String)
.split('\n')
.where((line) => line.isNotEmpty)
.length;
@@ -86,7 +90,7 @@ Future<void> updateBuildData() async {
Future<void> dartFormat() async {
final result = await fvmRun(['dart', 'format', '.']);
print('\n${result.stdout}');
print(result.stdout);
if (result.exitCode != 0) {
print(result.stderr);
exit(1);
@@ -119,7 +123,7 @@ Future<void> flutterBuild(
'--build-name=1.0.$build',
]);
}
print('[$buildType]\nBuilding with args: ${args.join(' ')}');
print('\n[$buildType]\nBuilding with args: ${args.join(' ')}');
final buildResult = await fvmRun(['flutter', ...args]);
final exitCode = buildResult.exitCode;
@@ -135,8 +139,6 @@ Future<void> flutterBuild(
exit(1);
}
}
print('Done.\n');
} else {
print(buildResult.stdout);
print(buildResult.stderr);
@@ -146,22 +148,26 @@ Future<void> flutterBuild(
}
Future<void> flutterBuildIOS() async {
await changeInfoPlistVersion();
await flutterBuild(
xcarchivePath, './release/${appName}_ios_build.xcarchive', 'ipa');
}
Future<void> flutterBuildMacOS() async {
await flutterBuild(
xcarchivePath, './release/${appName}_macos_build.xcarchive', 'macos');
}
Future<void> flutterBuildAndroid() async {
await flutterBuild(apkPath, './release/${appName}_build_Arm64.apk', 'apk');
}
Future<void> changeInfoPlistVersion() async {
for (final path in [iOSInfoPlistPath]) {
final file = File(path);
Future<void> changeAppleVersion() async {
for (final path in ['ios', 'macos']) {
final file = File(path + appleXCConfigPath);
final contents = await file.readAsString();
final newContents = contents
.replaceAll(regiOSMarketVer, 'MARKETING_VERSION = 1.0.$build;')
.replaceAll(regiOSProjectVer, 'CURRENT_PROJECT_VERSION = $build;');
.replaceAll(regAppleMarketVer, 'MARKETING_VERSION = 1.0.$build;')
.replaceAll(regAppleProjectVer, 'CURRENT_PROJECT_VERSION = $build;');
await file.writeAsString(newContents);
}
}
@@ -175,26 +181,31 @@ void main(List<String> args) async {
final command = args[0];
switch (command) {
case 'run':
return flutterRun(args.length == 2 ? args[1] : null);
case 'build':
final stopwatch = Stopwatch()..start();
build = await getGitCommitCount();
await updateBuildData();
await dartFormat();
await getGitCommitCount();
await changeAppleVersion();
await updateBuildData();
if (args.length > 1) {
final platform = args[1];
if (buildFuncs.keys.contains(platform)) {
await buildFuncs[platform]!();
} else {
print('Unknown platform: $platform');
final platforms = args[1];
for (final platform in platforms.split(',')) {
if (buildFuncs.keys.contains(platform)) {
await buildFuncs[platform]!();
print('Build finished in [${stopwatch.elapsed}]');
stopwatch.reset();
stopwatch.start();
} else {
print('Unknown platform: $platform');
}
}
return;
}
for (final func in buildFuncs.values) {
await func();
}
print('Build finished in ${stopwatch.elapsed}');
print('Build finished in ${stopwatch.elapsed}\n');
return;
default:
print('Unsupported command: $command');

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201"
sha256: e440ac42679dfc04bbbefb58ed225c994bc7e07fccc8a68ec7d3631a127e5da9
url: "https://pub.dev"
source: hosted
version: "52.0.0"
version: "54.0.0"
after_layout:
dependency: "direct main"
description:
@@ -21,26 +21,26 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4
sha256: "2c2e3721ee9fb36de92faa060f3480c81b23e904352b087e5c64224b1a044427"
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "5.6.0"
archive:
dependency: transitive
description:
name: archive
sha256: ed7cc591a948744994714375caf9a2ce89e1d82e8243997c8a2994d57181c212
sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
url: "https://pub.dev"
source: hosted
version: "3.3.5"
version: "3.3.6"
args:
dependency: transitive
description:
name: args
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
asn1lib:
dependency: transitive
description:
@@ -85,18 +85,18 @@ packages:
dependency: transitive
description:
name: build_daemon
sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf"
sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab"
sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
build_runner:
dependency: "direct dev"
description:
@@ -198,10 +198,10 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
url: "https://pub.dev"
source: hosted
version: "0.3.3+2"
version: "0.3.3+4"
crypto:
dependency: transitive
description:
@@ -262,10 +262,10 @@ packages:
dependency: "direct main"
description:
name: extended_image
sha256: d7e32f19a5c5ee3bfbcb3ff68492d3857bae00acd56e38048a8d53218871998b
sha256: a6b738d9b8d5513be72c545cc3e9c5c451fbee77c8db3cbec7c32ae85b82fb93
url: "https://pub.dev"
source: hosted
version: "6.3.4"
version: "6.4.1"
extended_image_library:
dependency: transitive
description:
@@ -298,14 +298,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.4"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9
url: "https://pub.dev"
source: hosted
version: "5.2.5"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec"
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@@ -336,18 +344,18 @@ packages:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "048bd1f1dc0e5ea25899f702815934d9a9e916fe23451c320e7dd94d5e3ad933"
sha256: e301ae206ff0fb09b67d3716009c6c28c2da57a0ad164827b178421bb9d601f7
url: "https://pub.dev"
source: hosted
version: "2.2.17"
version: "2.2.18"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b"
sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
version: "2.0.8"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -458,10 +466,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "1f9ecf99b08c92dedb699de1d28549f5d79b4423b48cf3f95d6968156db5dd67"
sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227"
url: "https://pub.dev"
source: hosted
version: "4.0.12"
version: "4.0.15"
intl:
dependency: "direct main"
description:
@@ -506,10 +514,10 @@ packages:
dependency: "direct main"
description:
name: logging
sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
matcher:
dependency: transitive
description:
@@ -570,50 +578,50 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95
sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9"
url: "https://pub.dev"
source: hosted
version: "2.0.12"
version: "2.0.13"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e
sha256: "7623b7d4be0f0f7d9a8b5ee6879fc13e4522d4c875ab86801dee4af32b54b83e"
url: "https://pub.dev"
source: hosted
version: "2.0.22"
version: "2.0.23"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74"
sha256: eec003594f19fe2456ea965ae36b3fc967bc5005f508890aafe31fa75e41d972
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.9"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
url: "https://pub.dev"
source: hosted
version: "2.0.5"
version: "2.0.6"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
pedantic:
dependency: transitive
description:
@@ -658,10 +666,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
pointycastle:
dependency: transitive
description:
@@ -730,10 +738,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: e387077716f80609bb979cd199331033326033ecd1c8f200a90c5f57b1c9f55e
sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
share_plus_platform_interface:
dependency: transitive
description:
@@ -775,10 +783,10 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d"
sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298
url: "https://pub.dev"
source: hosted
version: "1.2.6"
version: "1.2.7"
source_helper:
dependency: transitive
description:
@@ -863,74 +871,74 @@ packages:
dependency: transitive
description:
name: universal_io
sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef"
sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.2.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809"
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
url: "https://pub.dev"
source: hosted
version: "6.1.8"
version: "6.1.10"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1"
sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732"
url: "https://pub.dev"
source: hosted
version: "6.0.23"
version: "6.0.24"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3
sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5
url: "https://pub.dev"
source: hosted
version: "6.0.18"
version: "6.1.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc"
sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094"
sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6"
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0"
sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b"
url: "https://pub.dev"
source: hosted
version: "2.0.14"
version: "2.0.15"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615
sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.4"
uuid:
dependency: transitive
description:
@@ -975,10 +983,10 @@ packages:
dependency: transitive
description:
name: xdg_directories
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
url: "https://pub.dev"
source: hosted
version: "0.2.0+3"
version: "1.0.0"
xml:
dependency: transitive
description:
@@ -990,10 +998,9 @@ packages:
xterm:
dependency: "direct main"
description:
name: xterm
sha256: f65619cb24d03507812e346ddb8386cad9e16a01a481a8f5c8a2eba55b4edada
url: "https://pub.dev"
source: hosted
path: "../xterm.dart"
relative: true
source: path
version: "3.4.1"
yaml:
dependency: transitive
@@ -1004,5 +1011,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.0 <4.0.0"
dart: ">=2.19.0 <3.0.0"
flutter: ">=3.3.0"

View File

@@ -50,10 +50,13 @@ dependencies:
r_upgrade: ^0.3.6
path_provider: ^2.0.9
easy_isolate: ^1.3.0
share_plus: ^6.3.0
share_plus: ^6.3.1
intl: ^0.17.0
share_plus_web: ^3.1.0
xterm: ^3.4.1
# xterm: ^3.4.1
xterm:
path: ../xterm.dart
file_picker: ^5.2.5
dev_dependencies:
flutter_native_splash: ^2.1.6
@@ -73,7 +76,7 @@ dev_dependencies:
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
generate: true
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
@@ -180,5 +183,3 @@ flutter_native_splash:
# - 'ios/Runner/Info-Release.plist'
# To enable support for Android 12, set the following parameter to true. Defaults to false.
#android12: true
flutter_intl:
enabled: true

BIN
screenshots/detail.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -12,7 +12,7 @@ import 'package:toolbox/app.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);