Compare commits

...

54 Commits

Author SHA1 Message Date
Junyuan Feng
0533766c1c opt & fix
- update make script (taget mac)
- only set system overlay once
2022-05-19 16:06:12 +08:00
Junyuan Feng
36d7dc7bb2 opt
- optimize TextField
- opt apt pwd req times
- use logger in update.dart
2022-05-15 13:48:57 +08:00
Junyuan Feng
282443a548 migrate flutter3.0 2022-05-12 15:06:48 +08:00
Junyuan Feng
bfe7140a44 fix & opt
- fix docker not found regexp
- use en_US in docker cmd
-
2022-05-12 09:50:28 +08:00
Junyuan Feng
7e8e0e2efc optimized apt experience 2022-05-10 21:49:17 +08:00
Junyuan Feng
49f70fe41a fix update in multi-platform 2022-05-10 18:15:04 +08:00
Junyuan Feng
50762d585c fix & opt
- fix sftp downloaded page bottom path bg color error
- apt/yum add chinese
- urltext add textalign
2022-05-08 09:55:07 +08:00
Junyuan Feng
6f30976ef7 fix & opt
- sftp download unknown spent time
- shareFiles return bool
2022-05-08 09:07:03 +08:00
Junyuan Feng
b0c3cd3834 fix & optimize
- fix downloading page unbond size
- auto dispose isolate
2022-05-07 23:05:31 +08:00
Junyuan Feng
d224ad8cf8 Optimize
- divide mkdir/newfile
2022-05-07 22:36:32 +08:00
Junyuan Feng
b824e06736 New feat
- SFTP download
- open downloaded files in other apps
2022-05-07 22:15:09 +08:00
Junyuan Feng
74a933eb6e set LANG in shell 2022-05-05 16:53:35 +08:00
Junyuan Feng
29e3ee0156 i18n support 2022-05-05 16:07:55 +08:00
Junyuan Feng
f9aa3b1728 Fix scorll error. 2022-05-05 13:31:45 +08:00
Junyuan Feng
521ff998af Empty. 2022-05-04 21:08:03 +08:00
Junyuan Feng
b0165c3313 New splash bg color. 2022-05-04 20:35:56 +08:00
Junyuan Feng
2a0cc5ca30 Update & Fix
- update deps.
- splash support dark bg
- fix parse cpu temp
2022-05-04 19:44:13 +08:00
Junyuan Feng
52b82af52c Fix & Optimize
- fix ping when results is empty
- optimize ping speed
2022-05-03 20:26:00 +08:00
Junyuan Feng
5c59833233 Fix exception caused by LANG > Chinese 2022-05-03 12:06:18 +08:00
Junyuan Feng
9e2d49773f New Ping, use servers to ping 2022-05-03 11:42:30 +08:00
Junyuan Feng
fd1b2fc7b0 auto scroll on apt/yum upgrading. 2022-05-03 09:17:42 +08:00
Junyuan Feng
a06ea82f2c build script will update project.pbxproj versions. 2022-05-03 09:16:06 +08:00
Junyuan Feng
9b7d33369a fix
- docker ps item parse
- parse issue in fish (shell)
- make.dart path
2022-04-29 11:59:27 +08:00
Junyuan Feng
9663e4174d New make.dart 2022-04-15 19:48:43 +08:00
Junyuan Feng
c524db9f59 Improve animation by using SKSL 2022-04-15 19:21:40 +08:00
Junyuan Feng
4274e8bed1 Snippet support import/export. 2022-04-07 20:00:06 +08:00
Junyuan Feng
fa73c4feee Support yum. 2022-04-07 18:56:33 +08:00
Junyuan Feng
00cfd0f88d Remove SFTP download 2022-04-06 13:23:54 +08:00
Junyuan Feng
f8201f9542 SFTP support download. listDIr() support rollback. 2022-04-04 15:42:56 +08:00
Junyuan Feng
f0081e0587 Support APT/Docker 2022-03-10 15:25:14 +08:00
Junyuan Feng
e6e08dc407 Center title in SFTP/Apt 2022-03-08 18:07:52 +08:00
Junyuan Feng
7a5516792c Change SFTP to single column 2022-03-08 18:00:46 +08:00
Junyuan Feng
241002c3ea Support Docker start/stop/remove 2022-03-08 17:40:32 +08:00
Junyuan Feng
34e6b99297 APT/Docker manage
- view apt update
- view docker container
2022-03-08 14:47:57 +08:00
Junyuan Feng
b800bd91fd Fix saving not encypted key 2022-03-07 19:24:13 +08:00
Junyuan Feng
07fbf78853 Fix get mem failed in chinese 2022-03-02 11:14:32 +08:00
Junyuan Feng
bb80f093e0 Merge pull request #1 from RainSunMe/fix-get-mem
fix: can not get chinese mem
2022-03-02 10:56:40 +08:00
RainSun
fa8ffdc656 fix: can not get chinese mem 2022-03-02 10:53:01 +08:00
Junyuan Feng
f07d33a1d6 SFTP init. 2022-02-18 13:32:50 +08:00
Junyuan Feng
282e61afac 50% sftp 2022-02-10 20:27:30 +08:00
Junyuan Feng
feb3b10f1f Improve. 2022-02-09 11:29:56 +08:00
Junyuan Feng
1184841c24 Check drawer state when back. 2022-02-08 21:33:14 +08:00
Junyuan Feng
02a8a38a0a Update screenshots. 2022-02-08 21:21:35 +08:00
Junyuan Feng
d70cbb66d2 New UI. 2022-02-08 21:12:38 +08:00
Junyuan Feng
4636219b6a Update README 2022-02-08 19:18:21 +08:00
Junyuan Feng
b7b8d33bd9 Fix update server info failed. Display temp&uptime 2022-02-08 19:04:55 +08:00
Junyuan Feng
10c874e7d4 Migrate Flutter 2.10.0 2022-02-08 16:53:28 +08:00
Junyuan Feng
5bf5a4b67e Improve 2022-02-08 15:40:26 +08:00
Junyuan Feng
1bd43829bb Improve UI. Rounded Card. 2022-02-04 18:52:35 +08:00
Junyuan Feng
026d203608 Improve efficiency of data fetching 2022-02-02 13:23:54 +08:00
Junyuan Feng
5d9b19407f Auto unfocus to hide IME 2022-01-31 16:26:01 +08:00
Junyuan Feng
86be556a22 Improve 2022-01-19 16:54:08 +08:00
Junyuan Feng
e65d30590a Add Ping. Support launch page setting. 2022-01-18 13:35:00 +08:00
Junyuan Feng
86a700d0bb Optimize layout 2022-01-16 15:17:11 +08:00
117 changed files with 6042 additions and 853 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,31 +1,48 @@
# Server Monitor & Toolbox
A new Flutter project which provide a chart view to display server status data and a manager toolbox.
A new Flutter project which provide a chart view to display server status data.
## Milestone
- [x] Status chart view
- [x] Base64/Url En/Decode
- [x] Ping
- [x] Desktop support
- [x] Apt/Yum manager
- [x] SFTP
- [ ] Snippet market
- [x] Docker manager
- [x] i18n (English, Chinese)
## ScreenShots
<table>
<tr>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3327.PNG">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server.jpg">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3347.PNG">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_detail.jpg">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/detail.jpg">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_edit.jpg">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3330.PNG">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/convert.jpg">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3331.PNG">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/ping.jpg">
</td>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3346.PNG">
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/setting.jpg">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/drawer.jpg">
</td>
</tr>
</table>
@@ -36,27 +53,13 @@ Status|Platform
Full Support|Android/iOS/macOS
Support, but not tested|Windows/Linux
## Milestone
- [x] SSH connect
- [x] Server info store
- [x] Status chart view
- [x] Base64/Url En/Decode
- [x] Private key store
- [x] Server status detail page
- [x] Theme switch
- [x] Execute snippet
- [x] Migrate from `ssh2` to `dartssh2`
- [x] Desktop support
## Build
Please use `make.dart` to build.
```shell
# build android apk
./make.dart build android
# due to pub package 'ssh2' incompatibility
# can't build ios ipa through './make.dart build ios'
# more info: [https://github.com/jda258/flutter_ssh2/issues/8]
# please run below cmd to run on ios device
# build android apk and ios archive
./make.dart build
# Run in release mode
./make.dart run release
```

View File

@@ -22,6 +22,8 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
library_private_types_in_public_api: false
use_build_context_synchronously: false
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

View File

@@ -32,7 +32,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 30
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowSplashScreenBackground">#121212</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,7 +5,8 @@
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowFullscreen">true</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowSplashScreenBackground">#ffffff</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,7 +5,8 @@
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowFullscreen">true</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()

1
apk.sksl.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,19 +1,25 @@
PODS:
- countly_flutter (20.11.4):
- countly_flutter (21.11.0):
- Flutter
- Flutter (1.0.0)
- flutter_native_splash (0.0.1):
- Flutter
- path_provider_ios (0.0.1):
- Flutter
- r_upgrade (0.0.1):
- Flutter
- share_plus (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES:
@@ -21,20 +27,26 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/countly_flutter/ios"
Flutter:
:path: Flutter
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
r_upgrade:
:path: ".symlinks/plugins/r_upgrade/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
countly_flutter: 38419412e193a1faa5babeb5d28a63fda260687d
countly_flutter: e245f94349d8adf306c22e60c10648c69aae7380
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
COCOAPODS: 1.11.2
COCOAPODS: 1.11.3

View File

@@ -354,7 +354,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -362,7 +362,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.79;
MARKETING_VERSION = 1.0.136;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -484,7 +484,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -492,7 +492,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.79;
MARKETING_VERSION = 1.0.136;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -508,7 +508,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -516,7 +516,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.79;
MARKETING_VERSION = 1.0.136;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -5,11 +5,42 @@
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "darkbackground.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

View File

@@ -1,45 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ServerBox</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ServerBox</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>zh</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

1
ipa.sksl.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.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';
@@ -24,12 +27,20 @@ class MyApp extends StatelessWidget {
@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);
return MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
title: BuildData.name,
debugShowCheckedModeBanner: false,
theme: ThemeData(

View File

@@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:countly_flutter/countly_config.dart';
import 'package:countly_flutter/countly_flutter.dart';
import 'package:logging/logging.dart';
class Analysis {
static const _url = 'https://countly.xuty.cc';
@@ -9,12 +12,18 @@ class Analysis {
static bool _enabled = false;
static Future<void> init(bool debug) async {
_enabled = true;
await Countly.setLoggingEnabled(debug);
await Countly.init(_url, _key);
await Countly.start();
await Countly.enableCrashReporting();
await Countly.giveAllConsent();
if (Platform.isAndroid || Platform.isIOS) {
_enabled = true;
final config = CountlyConfig(_url, _key)
.setLoggingEnabled(debug)
.enableCrashReporting();
await Countly.initWithConfig(config);
await Countly.start();
await Countly.giveAllConsent();
} else {
Logger('COUNTLY')
.info('Unsupported platform ${Platform.operatingSystem}');
}
}
static void recordView(String view) {

View File

@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
extension ColorX on Color {
bool get isBrightColor {
return getBrightnessFromColor == Brightness.light;
}
Brightness get getBrightnessFromColor {
return ThemeData.estimateBrightnessForColor(this);
}
}

View File

@@ -0,0 +1,15 @@
extension NumX on num {
String get convertBytes {
const suffix = ['B', 'KB', 'MB', 'GB', 'TB'];
double value = toDouble();
int squareTimes = 0;
for (; value / 1024 > 1 && squareTimes < suffix.length - 1; squareTimes++) {
value /= 1024;
}
var finalValue = value.toStringAsFixed(1);
if (finalValue.endsWith('.0')) {
finalValue = finalValue.replaceFirst('.0', '');
}
return '$finalValue ${suffix[squareTimes]}';
}
}

View File

@@ -1,3 +1,63 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/model/distribution.dart';
extension StringX on String {
int get i => int.parse(this);
Distribution get dist {
final lower = toLowerCase();
for (var dist in debianDistList) {
if (lower.contains(dist)) {
return Distribution.debian;
}
}
for (var dist in rehlDistList) {
if (lower.contains(dist)) {
return Distribution.rehl;
}
}
return Distribution.unknown;
}
Uri get uri {
return Uri.parse(this);
}
Widget omitStartStr(
{TextStyle? style, TextOverflow? overflow, int? maxLines}) {
return LayoutBuilder(builder: (context, size) {
bool exceeded = false;
int len = 0;
for (; !exceeded && len < length; len++) {
// Build the textspan
var span = TextSpan(
text: 'A' * 7 + substring(length - len),
style: style ?? Theme.of(context).textTheme.bodyText2,
);
// Use a textpainter to determine if it will exceed max lines
var tp = TextPainter(
maxLines: maxLines ?? 1,
textDirection: TextDirection.ltr,
text: span,
);
// trigger it to layout
tp.layout(maxWidth: size.maxWidth);
// whether the text overflowed or not
exceeded = tp.didExceedMaxLines;
}
return Text(
(exceeded ? '...' : '') + substring(length - len),
overflow: overflow ?? TextOverflow.fade,
softWrap: false,
maxLines: maxLines ?? 1,
style: style,
);
});
}
String get withLangExport => 'export LANG=en_US.UTF-8 && $this';
}

View File

@@ -0,0 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
extension FutureUint8ListX on Future<Uint8List> {
Future<String> get string async => utf8.decode(await this);
}
extension Uint8ListX on Uint8List {
String get string => utf8.decode(this);
}

View File

@@ -1,22 +1,24 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.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';
final logger = Logger('UPDATE');
Future<bool> isFileAvailable(String url) async {
try {
final resp = await Dio().head(url);
return resp.statusCode == 200;
} catch (e) {
print('update file not available: $e');
logger.warning('update file not available: $e');
return false;
}
}
@@ -26,22 +28,35 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
locator<AppProvider>().setNewestBuild(update.newest);
if (!force && update.newest <= BuildData.build) {
print('Update ignored due to current: ${BuildData.build}, '
'update: ${update.newest}');
final newest = () {
if (Platform.isAndroid) {
return update.androidbuild;
} else if (Platform.isIOS) {
return update.iosbuild;
} else if (Platform.isMacOS) {
return update.macbuild;
}
return update.newest;
}();
if (!force && newest <= BuildData.build) {
logger.info('Update ignored due to current: ${BuildData.build}, '
'update: $newest');
return;
}
print('Update available: ${update.newest}');
logger.info('Update available: $newest');
if (Platform.isAndroid && !await isFileAvailable(update.android)) {
return;
}
final s = S.of(context);
showSnackBarWithAction(
context,
update.min > BuildData.build
? 'Your version is too old. \nPlease update to v1.0.${update.newest}.'
: 'Update: v1.0.${update.newest} available. \n${update.changelog}',
? 'Your version is too old. \nPlease update to v1.0.$newest.'
: 'Update: v1.0.$newest available. \n${update.changelog}',
'Update', () async {
if (Platform.isAndroid) {
await RUpgrade.upgrade(update.android,
@@ -51,5 +66,9 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
} else if (Platform.isMacOS) {
await RUpgrade.upgradeFromUrl(update.mac);
}
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
]);
});
}

View File

@@ -3,9 +3,12 @@ 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:toolbox/core/extension/stringx.dart';
void unawaited(Future<void> future) {}
@@ -27,10 +30,11 @@ void showSnackBarWithAction(BuildContext context, String content, String action,
}
Future<bool> openUrl(String url) async {
if (!await canLaunch(url)) {
final uri = url.uri;
if (!await canLaunchUrl(uri)) {
return false;
}
final ok = await launch(url, forceSafariVC: false);
final ok = await launchUrl(uri);
if (ok == true) {
return true;
}
@@ -71,12 +75,38 @@ Widget buildSwitch(BuildContext context, StoreProperty<bool> prop,
void setTransparentNavigationBar(BuildContext context) {
if (Platform.isAndroid) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarContrastEnforced: true,
systemNavigationBarIconBrightness:
isDarkMode(context) ? Brightness.light : Brightness.dark,
));
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarContrastEnforced: true));
}
}
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}';
}
await Share.shareFiles(filePaths, text: 'ServerBox -> $text');
return filePaths.isNotEmpty;
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.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,
),
],
);
}
class ServerTabMenuItems {
static const List<DropdownBtnItem> firstItems = [sftp, snippet, apt, docker];
static const List<DropdownBtnItem> secondItems = [edit];
static const sftp =
DropdownBtnItem(text: 'SFTP', icon: Icons.insert_drive_file);
static const snippet = DropdownBtnItem(text: 'Snippet', icon: Icons.label);
static const apt =
DropdownBtnItem(text: 'Apt/Yum', icon: Icons.system_security_update);
static const docker =
DropdownBtnItem(text: 'Docker', icon: Icons.view_agenda);
static const edit = DropdownBtnItem(text: 'Edit', icon: Icons.edit);
}
class DockerMenuItems {
static const rm = DropdownBtnItem(text: 'Remove', icon: Icons.delete);
static const start = DropdownBtnItem(text: 'Start', icon: Icons.play_arrow);
static const stop = DropdownBtnItem(text: 'Stop', icon: Icons.stop);
}

View File

@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class NavigationItem {
final IconData icon;
final String title;
NavigationItem(this.icon, this.title);
}

View File

@@ -0,0 +1,38 @@
class PathWithPrefix {
late String _prefixPath;
String _path = '/';
String? _prePath;
String get path => _prefixPath + _path;
PathWithPrefix(String prefixPath) {
if (prefixPath.endsWith('/')) {
_prefixPath = prefixPath.substring(0, prefixPath.length - 1);
} else {
_prefixPath = prefixPath;
}
}
void update(String newPath) {
_prePath = _path;
if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf('/'));
if (_path == '') {
_path = '/';
}
return;
}
if (newPath == '/') {
_path = '/';
return;
}
_path = _path + (_path.endsWith('/') ? '' : '/') + newPath;
}
bool undo() {
if (_prePath == null || _path == _prePath) {
return false;
}
_path = _prePath!;
return true;
}
}

View File

@@ -31,9 +31,9 @@ class AppUpdate {
});
AppUpdate.fromJson(Map<String, dynamic> json) {
newest = json["newest"]?.toInt();
newest = json["macbuild"]?.toInt();
newest = json["iosbuild"]?.toInt();
newest = json["androidbuild"]?.toInt();
macbuild = json["macbuild"]?.toInt();
iosbuild = json["iosbuild"]?.toInt();
androidbuild = json["androidbuild"]?.toInt();
android = json["android"].toString();
ios = json["ios"].toString();
min = json["min"].toInt();

View File

@@ -0,0 +1,41 @@
import 'package:toolbox/data/model/distribution.dart';
class UpgradePkgInfo {
final String _raw;
final Distribution _dist;
late String package;
late String nowVersion;
late String newVersion;
late String arch;
UpgradePkgInfo(this._raw, this._dist) {
switch (_dist) {
case Distribution.debian:
case Distribution.unknown:
_parseApt();
break;
case Distribution.rehl:
_parseYum();
}
}
void _parseApt() {
final split1 = _raw.split("/");
package = split1[0];
final split2 = split1[1].split(" ");
newVersion = split2[1];
arch = split2[2];
nowVersion = split2[5].replaceFirst(']', '');
}
void _parseYum() {
final result = RegExp(r'\S+').allMatches(_raw);
final pkgAndArch = result.elementAt(0).group(0) ?? '.';
final split1 = pkgAndArch.split('.');
package = split1[0];
arch = split1[1];
newVersion = result.elementAt(1).group(0) ?? 'Unknown';
nowVersion = '';
}
}

View File

@@ -0,0 +1,21 @@
enum Distribution {
unknown,
debian,
rehl,
}
const debianDistList = [
'debian',
'ubuntu',
'linuxmint',
'elementary',
'raspbian'
];
const rehlDistList = [
'redhat',
'fedora',
'centos',
'scientificlinux',
'rhel',
'oraclelinux'
];

View File

@@ -0,0 +1,37 @@
class DockerPsItem {
late String containerId;
late String image;
late String command;
late String created;
late String status;
late String ports;
late String name;
DockerPsItem(this.containerId, this.image, this.command, this.created,
this.status, this.ports, this.name);
DockerPsItem.fromRawString(String rawString) {
List<String> parts = rawString.split(RegExp(' +'));
parts = parts.map((e) => e.trim()).toList();
containerId = parts[0];
image = parts[1];
command = parts[2].trim();
created = parts[3];
status = parts[4];
if (running && parts.length > 6) {
ports = parts[5];
name = parts[6];
} else {
ports = '';
name = parts[5];
}
}
bool get running => status.contains('Up ');
@override
String toString() {
return 'DockerPsItem<$containerId@$name>';
}
}

View File

@@ -14,8 +14,10 @@ class Cpu2Status {
return used.isNaN ? 0 : 100 - used * 100;
}
Cpu2Status update(List<CpuStatus> newStatus, String newTemp) {
return Cpu2Status(now, newStatus, newTemp);
void update(List<CpuStatus> newStatus, String newTemp) {
pre = now;
now = newStatus;
temp = newTemp;
}
int get coresCount => now.length;

View File

@@ -1,4 +1,4 @@
import 'dart:math';
import 'package:toolbox/core/extension/numx.dart';
class NetSpeedPart {
String device;
@@ -21,51 +21,40 @@ class NetSpeed {
return devices;
}
NetSpeed update(List<NetSpeedPart> newOne) => NetSpeed(now, newOne);
void update(List<NetSpeedPart> newOne) {
old = now;
now = newOne;
}
int get timeDiff => now[0].time - old[0].time;
String speedIn({String? device}) {
if (old[0].device == '' || now[0].device == '') return '0kb/s';
int idx = 0;
if (device != null) {
for (var item in now) {
if (item.device == device) {
idx = now.indexOf(item);
break;
}
}
}
final idx = deviceIdx(device);
final speedInBytesPerSecond =
(now[idx].bytesIn - old[idx].bytesIn) / timeDiff;
int squareTimes = 0;
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
if (squareTimes >= suffixs.length - 1) break;
squareTimes++;
}
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
return buildStandardOutput(speedInBytesPerSecond);
}
String speedOut({String? device}) {
if (old[0].device == '' || now[0].device == '') return '0kb/s';
int idx = 0;
final idx = deviceIdx(device);
final speedInBytesPerSecond =
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
return buildStandardOutput(speedInBytesPerSecond);
}
int deviceIdx(String? device) {
if (device != null) {
for (var item in now) {
if (item.device == device) {
idx = now.indexOf(item);
break;
return now.indexOf(item);
}
}
}
final speedInBytesPerSecond =
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
int squareTimes = 0;
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
if (squareTimes >= suffixs.length - 1) break;
squareTimes++;
}
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
return 0;
}
}
const suffixs = ['b/s', 'kb/s', 'mb/s', 'gb/s'];
String buildStandardOutput(double speed) =>
'${speed.convertBytes.toLowerCase()}/s';
}

View File

@@ -0,0 +1,79 @@
final parseFailed = Exception('Parse failed');
final seqReg = RegExp(r'icmp_seq=(.+) ttl=(.+) time=(.+) ms');
final packetReg =
RegExp(r'(.+) packets transmitted, (.+) received, (.+)% packet loss');
final timeReg = RegExp(r'min/avg/max/mdev = (.+)/(.+)/(.+)/(.+) ms');
final ipReg = RegExp(r' \((\S+)\) ');
class PingResult {
String serverName;
String? ip;
List<PingSeqResult>? results;
PingStatistics? statistic;
PingResult.parse(this.serverName, String raw) {
final lines = raw.split('\n');
lines.removeWhere((element) => element.isEmpty);
final statisticIndex =
lines.indexWhere((element) => element.startsWith('---'));
if (statisticIndex == -1) {
throw parseFailed;
}
final statisticRaw = lines.sublist(statisticIndex + 1);
statistic = PingStatistics.parse(statisticRaw);
results = lines
.sublist(1, statisticIndex)
.map((e) => PingSeqResult.parse(e))
.toList();
ip = ipReg.firstMatch(lines[0])?.group(1);
}
}
class PingSeqResult {
int? seq;
int? ttl;
double? time;
PingSeqResult.parse(String raw) {
final seqMatched = seqReg.firstMatch(raw);
if (seqMatched == null) {
throw parseFailed;
}
seq = int.tryParse(seqMatched.group(1)!);
ttl = int.tryParse(seqMatched.group(2)!);
time = double.tryParse(seqMatched.group(3)!);
}
@override
String toString() {
return 'seq: $seq, ttl: $ttl, time: $time';
}
}
class PingStatistics {
int? total;
int? received;
double? loss;
double? min;
double? max;
double? avg;
double? stddev;
PingStatistics.parse(List<String> lines) {
if (lines.isEmpty || lines.length != 2) {
return;
}
final packetMatched = packetReg.firstMatch(lines[0]);
final timeMatched = timeReg.firstMatch(lines[1]);
if (packetMatched == null || timeMatched == null) {
return;
}
total = int.tryParse(packetMatched.group(1)!);
received = int.tryParse(packetMatched.group(2)!);
loss = double.tryParse(packetMatched.group(3)!);
min = double.tryParse(timeMatched.group(1)!);
avg = double.tryParse(timeMatched.group(2)!);
max = double.tryParse(timeMatched.group(3)!);
stddev = double.tryParse(timeMatched.group(4)!);
}
}

View File

@@ -0,0 +1,29 @@
class AbsolutePath {
String path;
String? _prePath;
AbsolutePath(this.path);
void update(String newPath) {
_prePath = path;
if (newPath == '..') {
path = path.substring(0, path.lastIndexOf('/'));
if (path == '') {
path = '/';
}
return;
}
if (newPath == '/') {
path = '/';
return;
}
path = path + (path.endsWith('/') ? '' : '/') + newPath;
}
bool undo() {
if (_prePath == null || _prePath == path) {
return false;
}
path = _prePath!;
return true;
}
}

View File

@@ -0,0 +1,14 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/absolute_path.dart';
class SftpBrowserStatus {
bool selected = false;
ServerPrivateInfo? spi;
List<SftpName>? files;
AbsolutePath? path;
SftpClient? client;
bool isBusy = false;
SftpBrowserStatus();
}

View File

@@ -0,0 +1,58 @@
import 'package:toolbox/data/model/sftp/download_worker.dart';
class SftpDownloadStatus {
final int id;
final DownloadItem item;
final void Function() notifyListeners;
late SftpDownloadWorker worker;
String get fileName => item.localPath.split('/').last;
// status of the download
double? progress;
SftpWorkerStatus? status;
int? size;
Exception? error;
Duration? spentTime;
SftpDownloadStatus(this.item, this.notifyListeners, {String? key})
: id = DateTime.now().microsecondsSinceEpoch {
worker =
SftpDownloadWorker(onNotify: onNotify, item: item, privateKey: key);
worker.init();
}
@override
bool operator ==(Object other) =>
other is SftpDownloadStatus && id == other.id;
@override
int get hashCode => id ^ super.hashCode;
void onNotify(dynamic event) {
switch (event.runtimeType) {
case SftpWorkerStatus:
status = event;
if (status == SftpWorkerStatus.finished) {
worker.dispose();
}
break;
case double:
progress = event;
break;
case int:
size = event;
break;
case Exception:
error = event;
break;
case Duration:
spentTime = event;
break;
default:
}
notifyListeners();
}
}
enum SftpWorkerStatus { preparing, sshConnectted, downloading, finished }

View File

@@ -0,0 +1,111 @@
import 'dart:async';
import 'dart:io';
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;
}
class SftpDownloadWorker {
SftpDownloadWorker(
{required this.onNotify, required this.item, this.privateKey});
final Function(Object event) onNotify;
final DownloadItem item;
final worker = Worker();
final String? privateKey;
void dispose() {
worker.dispose();
}
/// Initiate the worker (new thread) and start listen from messages between
/// the threads
Future<void> init() async {
if (worker.isInitialized) worker.dispose();
await worker.init(
mainMessageHandler,
isolateMessageHandler,
errorHandler: print,
);
worker.sendMessage(DownloadItemEvent(item, privateKey));
}
/// Handle the messages coming from the isolate
void mainMessageHandler(dynamic data, SendPort isolateSendPort) {
onNotify(data);
}
/// Handle the messages coming from the main
static isolateMessageHandler(
dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async {
if (data is DownloadItemEvent) {
try {
mainSendPort.send(SftpWorkerStatus.preparing);
final watch = Stopwatch()..start();
final item = data.item;
final spi = item.spi;
final socket = await SSHSocket.connect(spi.ip, spi.port);
SSHClient client;
if (spi.pubKeyId == null) {
client = SSHClient(socket,
username: spi.user,
onPasswordRequest: () => spi.authorization as String);
} else {
client = SSHClient(socket,
username: spi.user,
identities: SSHKeyPair.fromPem(data.privateKey!));
}
mainSendPort.send(SftpWorkerStatus.sshConnectted);
final remotePath = item.remotePath;
final localPath = item.localPath;
await Directory(localPath.substring(0, item.localPath.lastIndexOf('/')))
.create(recursive: true);
final local = File(localPath);
if (await local.exists()) {
await local.delete();
}
final localFile = local.openWrite(mode: FileMode.append);
final file = await (await client.sftp()).open(remotePath);
final size = (await file.stat()).size;
if (size == null) {
mainSendPort.send(Exception('can not get file size'));
return;
}
const defaultChunkSize = 1024 * 1024;
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
mainSendPort.send(size);
mainSendPort.send(SftpWorkerStatus.downloading);
for (var i = 0; i < size; i += chunkSize) {
final fileData = file.read(length: chunkSize);
await for (var form in fileData) {
localFile.add(form);
mainSendPort.send((i + form.length) / size * 100);
}
}
localFile.close();
mainSendPort.send(watch.elapsed);
mainSendPort.send(SftpWorkerStatus.finished);
} catch (e) {
mainSendPort.send(e);
}
}
}
}
class DownloadItemEvent {
DownloadItemEvent(this.item, this.privateKey);
final DownloadItem item;
final String? privateKey;
}

164
lib/data/provider/apt.dart Normal file
View File

@@ -0,0 +1,164 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:dartssh2/dartssh2.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/apt/upgrade_pkg_info.dart';
import 'package:toolbox/data/model/distribution.dart';
typedef PwdRequestFunc = Future<String> Function(
bool lastTimes, String? userName);
final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):');
class AptProvider extends BusyProvider {
final logger = Logger('AptProvider');
SSHClient? client;
Distribution? dist;
Function()? onUpgrade;
Function()? onUpdate;
PwdRequestFunc? onPasswordRequest;
String? whoami;
List<UpgradePkgInfo>? upgradeable;
String? error;
String? upgradeLog;
String? updateLog;
String lastLog = '';
int triedTimes = 0;
AptProvider();
Future<void> init(SSHClient client, Distribution dist, Function() onUpgrade,
Function() onUpdate, PwdRequestFunc onPasswordRequest) async {
this.client = client;
this.dist = dist;
this.onUpgrade = onUpgrade;
this.onPasswordRequest = onPasswordRequest;
whoami = (await client.run('whoami').string).trim();
}
bool get isSU => whoami == 'root';
void clear() {
client = null;
dist = null;
upgradeable = null;
error = null;
upgradeLog = null;
updateLog = whoami = null;
onUpgrade = null;
onUpdate = null;
onPasswordRequest = null;
triedTimes = 0;
}
Future<void> refreshInstalled() async {
if (client == null) {
error = 'No client';
return;
}
final result = await _update();
try {
getUpgradeableList(result);
} catch (e) {
error = '[Server Raw]:\n$result\n[App Error]:\n$e';
} finally {
notifyListeners();
}
}
void getUpgradeableList(String? raw) {
if (raw == null) return;
var list = raw.split('\n');
switch (dist) {
case Distribution.rehl:
list = list.sublist(2);
list.removeWhere((element) => element.isEmpty);
final endLine = list.lastIndexWhere(
(element) => element.contains('Obsoleting Packages'));
list = list.sublist(0, endLine);
break;
default:
list = list.sublist(4);
list.removeWhere((element) => element.isEmpty);
}
upgradeable = list.map((e) => UpgradePkgInfo(e, dist!)).toList();
}
Future<String> _update() async {
switch (dist) {
case Distribution.rehl:
return await client?.run(_wrap('yum check-update')).string ?? '';
default:
final session = await client!.execute(_wrap('apt update'));
session.stderr.listen((event) => _onPwd(event, session.stdin));
session.stdout.listen((event) {
updateLog = (updateLog ?? '') + event.string;
notifyListeners();
onUpdate ?? () {}();
});
await session.done;
return await client
?.run('apt list --upgradeable'.withLangExport)
.string ??
'';
}
}
Future<void> upgrade() async {
if (client == null) {
error = 'No client';
return;
}
final upgradeCmd = () {
switch (dist) {
case Distribution.rehl:
return 'yum upgrade -y';
default:
return 'apt upgrade -y';
}
}();
final session = await client!.execute(_wrap(upgradeCmd));
session.stderr.listen((e) => _onPwd(e, session.stdin));
session.stdout.listen((data) async {
final log = data.string;
if (lastLog == log.trim()) return;
upgradeLog = (upgradeLog ?? '') + log;
lastLog = log.trim();
notifyListeners();
onUpgrade!();
});
upgradeLog = null;
await session.done;
refreshInstalled();
}
Future<void> _onPwd(Uint8List e, StreamSink<Uint8List> stdin) async {
final event = e.string;
if (event.contains('[sudo] password for ')) {
final user = pwdRequestWithUserReg.firstMatch(event)?.group(1);
logger.info('sudo password request for $user');
triedTimes++;
final pwd =
await (onPasswordRequest ?? (_) async => '')(triedTimes == 3, user);
if (pwd.isEmpty) {
logger.info('sudo password request cancelled');
return;
}
stdin.add(Uint8List.fromList(utf8.encode('$pwd\n')));
}
}
String _wrap(String cmd) =>
'export LANG=en_US.utf-8 && ${isSU ? "" : "sudo -S "}$cmd';
}

View File

@@ -34,8 +34,8 @@ class DebugProvider extends ChangeNotifier {
),
);
_addWidget(SingleChildScrollView(
child: widget,
scrollDirection: Axis.horizontal,
child: widget,
));
}

View File

@@ -0,0 +1,107 @@
import 'package:dartssh2/dartssh2.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/docker/ps.dart';
final dockerNotFound = RegExp(r'command not found|Unknown command');
class DockerProvider extends BusyProvider {
SSHClient? client;
List<DockerPsItem>? items;
String? version;
String? edition;
String? error;
void init(SSHClient client) => this.client = client;
void clear() {
client = null;
error = null;
items = null;
version = null;
edition = null;
}
Future<void> refresh() async {
if (client == null) {
error = 'no client';
notifyListeners();
return;
}
final verRaw = await client!.run('docker version'.withLangExport).string;
if (verRaw.contains(dockerNotFound)) {
error = 'docker not found';
notifyListeners();
return;
}
final verSplit = verRaw.split('\n');
if (verSplit.length < 3) {
error = 'invalid version';
notifyListeners();
return;
} else {
try {
version = verSplit[1].split(' ').last;
edition = verSplit[0].split(': ')[1];
} catch (e) {
error = e.toString();
return;
}
}
final raw = await client!.run('docker ps -a'.withLangExport).string;
final lines = raw.split('\n');
lines.removeAt(0);
lines.removeWhere((element) => element.isEmpty);
try {
items = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
} catch (e) {
error = e.toString();
rethrow;
} finally {
notifyListeners();
}
}
Future<bool> stop(String id) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker stop $id').string;
await refresh();
setBusyState(false);
return result.contains(id);
}
Future<bool> start(String id) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker start $id').string;
await refresh();
setBusyState(false);
return result.contains(id);
}
Future<bool> delete(String id) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker rm $id').string;
await refresh();
setBusyState(false);
return result.contains(id);
}
}

View File

@@ -1,10 +1,10 @@
import 'dart:async';
import 'dart:convert';
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';
@@ -26,13 +26,24 @@ import 'package:toolbox/locator.dart';
/// 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) {
final watch = Stopwatch()..start();
final pem = SSHKeyPair.fromPem(key);
watch.stop();
print('loadIndentity: ${watch.elapsedMilliseconds}ms');
return pem;
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"
"free -m \necho $seperator \n"
"cat /sys/class/thermal/thermal_zone*/type \necho $seperator \n"
"cat /sys/class/thermal/thermal_zone*/temp";
const shellPath = '.serverbox.sh';
const memPrefix = 'Mem:';
final cpuTempReg = RegExp('(x86_pkg_temp|cpu_thermal)');
class ServerProvider extends BusyProvider {
List<ServerInfo> _servers = [];
List<ServerInfo> get servers => _servers;
@@ -89,22 +100,14 @@ class ServerProvider extends BusyProvider {
identities: await compute(loadIndentity, key.privateKey));
}
Future<void> refreshData({int? idx}) async {
if (idx != null) {
_getData(idx);
Future<void> refreshData({ServerPrivateInfo? spi}) async {
if (spi != null) {
_getData(spi);
return;
}
try {
await Future.wait(_servers.map((s) async {
final idx = _servers.indexOf(s);
if (idx == -1) return;
await _getData(idx);
}));
} catch (e) {
if (e is! RangeError) {
rethrow;
}
}
await Future.wait(_servers.map((s) async {
await _getData(s.info);
}));
}
Future<void> startAutoRefresh() async {
@@ -124,15 +127,22 @@ class ServerProvider extends BusyProvider {
}
}
void addServer(ServerPrivateInfo info) {
_servers.add(genInfo(info));
locator<ServerStore>().put(info);
void setDisconnected() {
for (var i = 0; i < _servers.length; i++) {
_servers[i].connectionState = ServerConnectionState.disconnected;
}
}
void addServer(ServerPrivateInfo spi) {
_servers.add(genInfo(spi));
locator<ServerStore>().put(spi);
notifyListeners();
refreshData(idx: _servers.length - 1);
refreshData(spi: spi);
}
void delServer(ServerPrivateInfo info) {
final idx = _servers.indexWhere((s) => s.info == info);
if (idx == -1) return;
_servers[idx].client?.close();
_servers.removeAt(idx);
notifyListeners();
@@ -140,54 +150,67 @@ class ServerProvider extends BusyProvider {
}
Future<void> updateServer(
ServerPrivateInfo old, ServerPrivateInfo newInfo) async {
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
final idx = _servers.indexWhere((e) => e.info == old);
_servers[idx].info = newInfo;
_servers[idx].client = await genClient(newInfo);
locator<ServerStore>().update(old, newInfo);
if (idx < 0) {
throw RangeError.index(idx, _servers);
}
_servers[idx].info = newSpi;
_servers[idx].client = await genClient(newSpi);
locator<ServerStore>().update(old, newSpi);
notifyListeners();
refreshData(idx: idx);
refreshData(spi: newSpi);
}
Future<void> _getData(int idx) async {
final client = _servers[idx].client;
final info = _servers[idx].info;
final connected = client != null;
Future<void> _getData(ServerPrivateInfo spi) async {
final idx = _servers.indexWhere((element) => element.info == spi);
final state = _servers[idx].connectionState;
if (!connected ||
if (_servers[idx].client == null ||
state == ServerConnectionState.failed ||
state == ServerConnectionState.disconnected) {
_servers[idx].connectionState = ServerConnectionState.connecting;
notifyListeners();
final time1 = DateTime.now();
try {
_servers[idx].client = await genClient(info);
_servers[idx].client = await genClient(spi);
final time2 = DateTime.now();
logger.info(
'Connected to [${info.name}] in [${time2.difference(time1).toString()}].');
'Connected to [${spi.name}] in [${time2.difference(time1).toString()}].');
_servers[idx].connectionState = ServerConnectionState.connected;
notifyListeners();
_servers[idx]
.client!
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath");
} catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed;
_servers[idx].status.failedInfo = e.toString();
notifyListeners();
_servers[idx].status.failedInfo = '$e ## ';
logger.warning(e);
} finally {
notifyListeners();
}
}
// if client is null, return
final si = _servers[idx];
if (si.client == null) return;
final raw = await si.client!.run("sh $shellPath").string;
final lines = raw.split(seperator).map((e) => e.trim()).toList();
lines.removeAt(0);
try {
if (_servers[idx].client == null) return;
_getCPU(_servers[idx]);
_getMem(_servers[idx]);
_getSysVer(_servers[idx]);
_getUpTime(_servers[idx]);
_getDisk(_servers[idx]);
_getTcp(_servers[idx]);
_getNetSpeed(_servers[idx]);
_getCPU(spi, lines[2], lines[7], lines[8]);
_getMem(spi, lines[6]);
_getSysVer(spi, lines[1]);
_getUpTime(spi, lines[3]);
_getDisk(spi, lines[5]);
_getTcp(spi, lines[4]);
_getNetSpeed(spi, lines[0]);
} catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed;
servers[idx].status.failedInfo = e.toString();
notifyListeners();
logger.warning(e);
rethrow;
} finally {
notifyListeners();
}
}
@@ -197,14 +220,12 @@ class ServerProvider extends BusyProvider {
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901
Future<void> _getNetSpeed(ServerInfo info) async {
final idx = _servers.indexOf(info);
final client = _servers[idx].client!;
final raw = utf8.decode(await client.run('cat /proc/net/dev && date +%s'));
void _getNetSpeed(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final split = raw.split('\n');
final deviceCount = split.length - 3;
if (deviceCount < 1) return;
final time = int.parse(split[split.length - 2]);
final time = int.parse(split[split.length - 1]);
final results = <NetSpeedPart>[];
for (int idx = 2; idx < deviceCount; idx++) {
final data = split[idx].trim().split(':');
@@ -215,41 +236,40 @@ class ServerProvider extends BusyProvider {
final bytesOut = int.parse(bytes[8]);
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
}
info.status.netSpeed = info.status.netSpeed.update(results);
notifyListeners();
info.status.netSpeed.update(results);
}
Future<void> _getSysVer(ServerInfo info) async {
final idx = _servers.indexOf(info);
final client = _servers[idx].client!;
final raw =
utf8.decode(await client.run('cat /etc/os-release | grep PRETTY_NAME'));
void _getSysVer(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final s = raw.split('=');
if (s.length == 2) {
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
} else {
info.status.sysVer = '';
}
notifyListeners();
}
String _getCPUTemp(String raw) {
final split = raw.split('\n');
String _getCPUTemp(String type, String value) {
const noMatch = "/sys/class/thermal/thermal_zone*/type";
// Not support to get CPU temperature
if (value.contains(noMatch) ||
type.contains(noMatch) ||
value.isEmpty ||
type.isEmpty) {
return '';
}
final split = type.split('\n');
int idx = 0;
for (var item in split) {
if (item.contains('x86_pkg_temp') || item.contains('cpu_thermal')) {
return item.split(' ').last;
if (item.contains(cpuTempReg)) {
break;
}
idx++;
}
return '';
return '${(int.parse(value.split('\n')[idx].trim()) / 1000).toStringAsFixed(1)}°C';
}
Future<void> _getCPU(ServerInfo info) async {
final idx = _servers.indexOf(info);
final client = _servers[idx].client!;
final raw = utf8.decode(await client.run("cat /proc/stat | grep cpu"));
final temp = utf8.decode(await client.run(
r"paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/'"));
void _getCPU(
ServerPrivateInfo spi, String raw, String tempType, String tempValue) {
final info = _servers.firstWhere((e) => e.info == spi);
final List<CpuStatus> cpus = [];
for (var item in raw.split('\n')) {
@@ -266,47 +286,31 @@ class ServerProvider extends BusyProvider {
int.parse(matches[5]),
int.parse(matches[6])));
}
if (cpus.isEmpty) {
info.status.cpu2Status = emptyCpu2Status;
} else {
info.status.cpu2Status =
info.status.cpu2Status.update(cpus, _getCPUTemp(temp));
if (cpus.isNotEmpty) {
info.status.cpu2Status.update(cpus, _getCPUTemp(tempType, tempValue));
}
notifyListeners();
}
Future<void> _getUpTime(ServerInfo info) async {
final idx = _servers.indexOf(info);
final client = _servers[idx].client!;
final raw = utf8.decode(await client.run('uptime'));
info.status.uptime = raw.split('up ')[1].split(', ')[0];
notifyListeners();
void _getUpTime(ServerPrivateInfo spi, String raw) {
_servers.firstWhere((e) => e.info == spi).status.uptime =
raw.split('up ')[1].split(', ')[0];
}
Future<void> _getTcp(ServerInfo info) async {
final sidx = _servers.indexOf(info);
final client = _servers[sidx].client!;
final raw = utf8.decode(await client.run('cat /proc/net/snmp'));
void _getTcp(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final lines = raw.split('\n');
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
orElse: () => '');
if (idx != '') {
final vals = idx.split(RegExp(r'\s{1,}'));
info.status.tcp = TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
} else {
info.status.tcp = TcpStatus(0, 0, 0, 0);
}
notifyListeners();
}
Future<void> _getDisk(ServerInfo info) async {
final idx = _servers.indexOf(info);
final client = _servers[idx].client!;
final disk = utf8.decode(await client.run('df -h'));
void _getDisk(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final list = <DiskInfo>[];
final items = disk.split('\n');
final items = raw.split('\n');
for (var item in items) {
if (items.indexOf(item) == 0 || item.isEmpty) {
continue;
@@ -316,16 +320,13 @@ class ServerProvider extends BusyProvider {
int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3]));
}
info.status.disk = list;
notifyListeners();
}
Future<void> _getMem(ServerInfo info) async {
final idx = _servers.indexOf(info);
final client = _servers[idx].client!;
final mem = utf8.decode(await client.run('free -m'));
for (var item in mem.split('\n')) {
if (item.contains('Mem:')) {
final split = item.replaceFirst('Mem:', '').split(' ');
void _getMem(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
for (var item in raw.split('\n')) {
if (item.contains(memPrefix)) {
final split = item.replaceFirst(memPrefix, '').split(' ');
split.removeWhere((e) => e == '');
final memList = split.map((e) => int.parse(e)).toList();
info.status.memory = Memory(
@@ -335,13 +336,16 @@ class ServerProvider extends BusyProvider {
shared: memList[3],
cache: memList[4],
avail: memList[5]);
break;
}
}
notifyListeners();
}
Future<String?> runSnippet(int idx, Snippet snippet) async {
final result = await _servers[idx].client!.run(snippet.script);
return utf8.decode(result);
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
return await _servers
.firstWhere((element) => element.info == spi)
.client!
.run(snippet.script)
.string;
}
}

View File

@@ -0,0 +1,31 @@
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';
class SftpDownloadProvider extends ProviderBase {
final List<SftpDownloadStatus> _status = [];
List<SftpDownloadStatus> get status => _status;
List<SftpDownloadStatus> gets({int? id, String? fileName}) {
var found = <SftpDownloadStatus>[];
if (id != null) {
found = _status.where((e) => e.id == id).toList();
}
if (fileName != null) {
found = found
.where((e) => e.item.localPath.split('/').last == fileName)
.toList();
}
return found;
}
SftpDownloadStatus? get({int? id, String? name}) {
final found = gets(id: id, fileName: name);
if (found.isEmpty) return null;
return found.first;
}
void add(DownloadItem item, {String? key}) {
_status.add(SftpDownloadStatus(item, notifyListeners, key: key));
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/store/snippet.dart';
@@ -11,22 +13,34 @@ class SnippetProvider extends BusyProvider {
_snippets = locator<SnippetStore>().fetch();
}
void addInfo(Snippet snippet) {
void add(Snippet snippet) {
if (have(snippet)) return;
_snippets.add(snippet);
locator<SnippetStore>().put(snippet);
notifyListeners();
}
void delInfo(Snippet snippet) {
_snippets.removeWhere((e) => e.name == snippet.name);
void del(Snippet snippet) {
if (!have(snippet)) return;
_snippets.removeAt(index(snippet));
locator<SnippetStore>().delete(snippet);
notifyListeners();
}
void updateInfo(Snippet old, Snippet newOne) {
final idx = _snippets.indexWhere((e) => e.name == old.name);
_snippets[idx] = newOne;
int index(Snippet snippet) {
return _snippets.indexWhere((e) => e.name == snippet.name);
}
bool have(Snippet snippet) {
return index(snippet) != -1;
}
void update(Snippet old, Snippet newOne) {
if (!have(old)) return;
_snippets[index(old)] = newOne;
locator<SnippetStore>().update(old, newOne);
notifyListeners();
}
String get export => json.encode(snippets);
}

View File

@@ -1,9 +1,10 @@
// This file is generated by ./make.dart
class BuildData {
static const String name = "ToolBox";
static const int build = 82;
static const String engine = "Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (4 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
static const String buildAt = "2022-01-11 13:31:16.586591";
static const int modifications = 3;
static const String name = "ServerBox";
static const int build = 136;
static const String engine =
"Flutter 3.0.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision ee4e09cce0 (9 days ago) • 2022-05-09 16:45:18 -0700\nEngine • revision d1b9a6938a\nTools • Dart 2.17.0 • DevTools 2.12.2\n";
static const String buildAt = "2022-05-19 15:59:10.728748";
static const int modifications = 15;
}

View File

@@ -0,0 +1,4 @@
import 'package:flutter/material.dart';
const TextStyle size18 = TextStyle(fontSize: 18);
const TextStyle grey = TextStyle(color: Colors.grey);

View File

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

10
lib/data/res/path.dart Normal file
View File

@@ -0,0 +1,10 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<Directory> get docDir async => await getApplicationDocumentsDirectory();
Future<Directory> get sftpDownloadDir async {
final dir = Directory('${(await docDir).path}/sftp');
return dir.create(recursive: true);
}

9
lib/data/res/tab.dart Normal file
View File

@@ -0,0 +1,9 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/model/app/navigation_item.dart';
final List<String> tabs = ['Servers', 'En/Decode', 'Ping'];
final List<NavigationItem> tabItems = [
NavigationItem(Icons.cloud, 'Server'),
NavigationItem(Icons.code, 'Convert'),
NavigationItem(Icons.leak_add, 'Ping'),
];

View File

@@ -1,5 +1,6 @@
const backendUrl = 'https://res.lolli.tech';
const baseUrl = backendUrl + '/toolbox';
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 issueUrl = 'https://github.com/LollipopKit/flutter_server_box/issues';

View File

@@ -23,7 +23,11 @@ class ServerStore extends PersistentStore {
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
final ss = fetch();
ss[index(old)] = newInfo;
final idx = index(old);
if (idx < 0) {
throw RangeError.index(idx, ss);
}
ss[idx] = newInfo;
box.put('servers', json.encode(ss));
}

View File

@@ -5,5 +5,6 @@ class SettingStore extends PersistentStore {
StoreProperty<int> get primaryColor =>
property('primaryColor', defaultValue: Colors.deepPurpleAccent.value);
StoreProperty<int> get serverStatusUpdateInterval =>
property('serverStatusUpdateInterval', defaultValue: 3);
property('serverStatusUpdateInterval', defaultValue: 2);
StoreProperty<int> get launchPage => property('launchPage', defaultValue: 0);
}

View File

@@ -0,0 +1,66 @@
// 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: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 Future.value(null),
'zh': () => new Future.value(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) async {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new Future.value(false);
}
var lib = _deferredLibraries[availableLocale];
await (lib == null ? new Future.value(false) : lib());
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new Future.value(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

@@ -0,0 +1,213 @@
// 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(rainSunMeGithub) =>
"\nThanks ${rainSunMeGithub} for participating in the test.\n\nAll rights reserved.";
static String m1(fileName) => "Download [${fileName}] to local?";
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(time) => "Spent time: ${time}";
static String m11(name) => "Are you sure to delete [${name}]?";
static String m12(server) => "Are you sure to delete server [${server}]?";
static String m13(build) => "Found: v1.0.${build}, click to update";
static String m14(build) => "Current: v1.0.${build}";
static String m15(build) => "Current: v1.0.${build}, is up to date";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": m0,
"addAServer": MessageLookupByLibrary.simpleMessage("add a server"),
"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"),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
"chooseDestination":
MessageLookupByLibrary.simpleMessage("Choose destination"),
"choosePrivateKey":
MessageLookupByLibrary.simpleMessage("Choose private key"),
"clear": MessageLookupByLibrary.simpleMessage("Clear"),
"close": MessageLookupByLibrary.simpleMessage("Close"),
"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": m1,
"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"),
"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?"),
"host": MessageLookupByLibrary.simpleMessage("Host"),
"httpFailedWithCode": m6,
"import": MessageLookupByLibrary.simpleMessage("Import"),
"importAndExport":
MessageLookupByLibrary.simpleMessage("Import and Export"),
"install": MessageLookupByLibrary.simpleMessage("install"),
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
"Please https://docs.docker.com/engine/install docker first."),
"invalidVersionHelp": m7,
"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"),
"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"),
"open": MessageLookupByLibrary.simpleMessage("Open"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage(
"Please input a target IP/domain."),
"platformNotSupportUpdate": MessageLookupByLibrary.simpleMessage(
"Current platform does not support in app update.\nPlease build from source and install it."),
"plzEnterHost":
MessageLookupByLibrary.simpleMessage("Please enter host."),
"plzEnterPwd":
MessageLookupByLibrary.simpleMessage("Please enter password."),
"plzSelectKey":
MessageLookupByLibrary.simpleMessage("Please select a key."),
"port": MessageLookupByLibrary.simpleMessage("Port"),
"privateKey": MessageLookupByLibrary.simpleMessage("Private Key"),
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
"reportBugsOnGithubIssue": m9,
"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"),
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
"spentTime": m10,
"start": MessageLookupByLibrary.simpleMessage("Start"),
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
"sureDelete": m11,
"sureToDeleteServer": m12,
"ttl": MessageLookupByLibrary.simpleMessage("TTL"),
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
"unkownConvertMode":
MessageLookupByLibrary.simpleMessage("Unknown convert mode"),
"updateAll": MessageLookupByLibrary.simpleMessage("Update all"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"You set to 0, will not update automatically.\nYou can pull to refresh manually."),
"updateServerStatusInterval": MessageLookupByLibrary.simpleMessage(
"Server status update interval"),
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
"user": MessageLookupByLibrary.simpleMessage("User"),
"versionHaveUpdate": m13,
"versionUnknownUpdate": m14,
"versionUpdated": m15,
"waitConnection": MessageLookupByLibrary.simpleMessage(
"Please wait for the connection to be established."),
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("Will take effect immediately")
};
}

View File

@@ -0,0 +1,185 @@
// 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(rainSunMeGithub) =>
"\n感谢 ${rainSunMeGithub} 参与软件测试。\n\n保留所有权利。";
static String m1(fileName) => "下载 [${fileName}] 到本地?";
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(time) => "耗时: ${time}";
static String m11(name) => "确定删除[${name}]";
static String m12(server) => "你确定要删除服务器 [${server}] 吗?";
static String m13(build) => "找到新版本v1.0.${build}, 点击更新";
static String m14(build) => "当前v1.0.${build}";
static String m15(build) => "当前v1.0.${build}, 已是最新版本";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"aboutThanks": m0,
"addAServer": MessageLookupByLibrary.simpleMessage("添加服务器"),
"addPrivateKey": MessageLookupByLibrary.simpleMessage("添加一个私钥"),
"alreadyLastDir": MessageLookupByLibrary.simpleMessage("已经是最上层目录了"),
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
"attention": MessageLookupByLibrary.simpleMessage("注意"),
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
"choose": MessageLookupByLibrary.simpleMessage("选择"),
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
"choosePrivateKey": MessageLookupByLibrary.simpleMessage("选择私钥"),
"clear": MessageLookupByLibrary.simpleMessage("清除"),
"close": 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": m1,
"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("导出"),
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage("这些输入框不能为空。"),
"files": MessageLookupByLibrary.simpleMessage("文件"),
"foundNUpdate": m5,
"go": MessageLookupByLibrary.simpleMessage("开始"),
"goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"),
"host": MessageLookupByLibrary.simpleMessage("主机"),
"httpFailedWithCode": m6,
"import": MessageLookupByLibrary.simpleMessage("导入"),
"importAndExport": MessageLookupByLibrary.simpleMessage("导入或导出"),
"install": MessageLookupByLibrary.simpleMessage("安装"),
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
"请先 https://docs.docker.com/engine/install docker"),
"invalidVersionHelp": m7,
"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("名称"),
"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(""),
"open": MessageLookupByLibrary.simpleMessage("打开"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("平均:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"),
"platformNotSupportUpdate":
MessageLookupByLibrary.simpleMessage("当前平台不支持更新,请编译最新源码后手动安装"),
"plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"),
"plzEnterPwd": MessageLookupByLibrary.simpleMessage("请输入密码"),
"plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"),
"port": MessageLookupByLibrary.simpleMessage("端口"),
"privateKey": MessageLookupByLibrary.simpleMessage("私钥"),
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
"reportBugsOnGithubIssue": m9,
"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 已连接,即将开始下载..."),
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
"spentTime": m10,
"start": MessageLookupByLibrary.simpleMessage("开始"),
"stop": MessageLookupByLibrary.simpleMessage("停止"),
"sureDelete": m11,
"sureToDeleteServer": m12,
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
"unkownConvertMode": MessageLookupByLibrary.simpleMessage("未知转换模式"),
"updateAll": MessageLookupByLibrary.simpleMessage("更新全部"),
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
"你设置为0服务器状态不会自动刷新。\n你可以手动下拉刷新。"),
"updateServerStatusInterval":
MessageLookupByLibrary.simpleMessage("服务器状态刷新间隔"),
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
"user": MessageLookupByLibrary.simpleMessage("用户"),
"versionHaveUpdate": m13,
"versionUnknownUpdate": m14,
"versionUpdated": m15,
"waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"),
"willTakEeffectImmediately":
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
};
}

1250
lib/generated/l10n.dart Normal file

File diff suppressed because it is too large Load Diff

119
lib/l10n/intl_en.arb Normal file
View File

@@ -0,0 +1,119 @@
{
"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": "\nThanks {rainSunMeGithub} for participating in the test.\n\nAll rights reserved.",
"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.\nYou can pull to refresh manually.",
"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.",
"plzEnterPwd": "Please enter 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!"
}

119
lib/l10n/intl_zh.arb Normal file
View File

@@ -0,0 +1,119 @@
{
"server": "服务器",
"convert": "转换",
"ping": "Ping",
"debug": "调试",
"addAServer": "添加服务器",
"setting": "设置",
"license": "开源证书",
"snippet": "代码片段",
"privateKey": "私钥",
"madeWithLove": "\n用❤制作 by {myGithub}",
"aboutThanks": "\n感谢 {rainSunMeGithub} 参与软件测试。\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你可以手动下拉刷新。",
"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": "请输入主机",
"plzEnterPwd": "请输入密码",
"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": "最后尝试"
}

View File

@@ -1,8 +1,11 @@
import 'package:get_it/get_it.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/apt.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/private_key.dart';
@@ -18,10 +21,13 @@ void setupLocatorForServices() {
void setupLocatorForProviders() {
locator.registerSingleton(AppProvider());
locator.registerSingleton(AptProvider());
locator.registerSingleton(DebugProvider());
locator.registerSingleton(DockerProvider());
locator.registerSingleton(ServerProvider());
locator.registerSingleton(SnippetProvider());
locator.registerSingleton(PrivateKeyProvider());
locator.registerSingleton(SftpDownloadProvider());
}
Future<void> setupLocatorForStores() async {

View File

@@ -7,9 +7,12 @@ import 'package:provider/provider.dart';
import 'package:toolbox/app.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/apt.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/locator.dart';
@@ -62,10 +65,14 @@ Future<void> main() async {
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
ChangeNotifierProvider(create: (_) => locator<AptProvider>()),
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
ChangeNotifierProvider(create: (_) => locator<DockerProvider>()),
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
ChangeNotifierProvider(
create: (_) => locator<SftpDownloadProvider>()),
],
child: const MyApp(),
),

236
lib/view/page/apt.dart Normal file
View File

@@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/apt.dart';
import 'package:toolbox/data/provider/server.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';
class AptManagePage extends StatefulWidget {
const AptManagePage(this.spi, {Key? key}) : super(key: key);
final ServerPrivateInfo spi;
@override
_AptManagePageState createState() => _AptManagePageState();
}
class _AptManagePageState extends State<AptManagePage>
with SingleTickerProviderStateMixin {
late MediaQueryData _media;
final greyStyle = const TextStyle(color: Colors.grey);
final scrollController = ScrollController();
final scrollControllerUpdate = ScrollController();
final _aptProvider = locator<AptProvider>();
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
_aptProvider.refreshInstalled();
}
@override
void dispose() {
super.dispose();
locator<AptProvider>().clear();
}
@override
void initState() {
super.initState();
final si = locator<ServerProvider>()
.servers
.firstWhere((e) => e.info == widget.spi);
if (si.client == null) {
showSnackBar(context, Text(s.waitConnection));
Navigator.of(context).pop();
return;
}
// ignore: prefer_function_declarations_over_variables
PwdRequestFunc onPwdRequest = (lastTime, user) async {
if (!mounted) return '';
final textController = TextEditingController();
await showRoundDialog(
context,
lastTime ? s.lastTry : (user ?? s.unknown),
TextField(
controller: textController,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
onSubmitted: (_) => textController.text.trim(),
decoration: InputDecoration(
labelText: s.pwd,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
Navigator.of(context).pop();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
]);
return textController.text.trim();
};
_aptProvider.init(
si.client!,
si.status.sysVer.dist,
() =>
scrollController.jumpTo(scrollController.position.maxScrollExtent),
() => scrollControllerUpdate
.jumpTo(scrollControllerUpdate.position.maxScrollExtent),
onPwdRequest);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: 'Apt', down: widget.spi.name),
),
body: Consumer<AptProvider>(builder: (_, apt, __) {
if (apt.error != null) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error,
color: Colors.redAccent,
size: 37,
),
const SizedBox(
height: 37,
),
Text(
apt.error!,
textAlign: TextAlign.center,
),
],
);
}
if (apt.updateLog == null && apt.upgradeable == null) {
return centerLoading;
}
if (apt.updateLog != null && apt.upgradeable == null) {
return SizedBox(
height: _media.size.height * 0.7,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(18),
controller: scrollControllerUpdate,
child: Text(apt.updateLog!),
),
));
}
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(),
);
}),
);
}
Widget _buildUpdatePanel(AptProvider apt) {
if (apt.upgradeable!.isEmpty) {
return ListTile(
title: Text(
s.noUpdateAvailable,
textAlign: TextAlign.center,
),
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: greyStyle,
),
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!),
),
))
],
)
],
);
}
Widget _buildUpdateItem(UpgradePkgInfo info, AptProvider apt) {
return ListTile(
title: Text(info.package),
subtitle: Text(
'${info.nowVersion} -> ${info.newVersion}',
style: greyStyle,
),
);
}
}

View File

@@ -4,6 +4,9 @@ import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.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';
class ConvertPage extends StatefulWidget {
const ConvertPage({Key? key}) : super(key: key);
@@ -18,13 +21,8 @@ class _ConvertPageState extends State<ConvertPage>
late TextEditingController _textEditingControllerResult;
late MediaQueryData _media;
late ThemeData _theme;
late S s;
static const List<String> _typeOption = [
'base64 decode',
'base64 encode',
'URL encode',
'URL decode'
];
int _typeOptionIndex = 0;
@override
@@ -39,6 +37,7 @@ class _ConvertPageState extends State<ConvertPage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
s = S.of(context);
}
@override
@@ -48,6 +47,7 @@ class _ConvertPageState extends State<ConvertPage>
body: GestureDetector(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7),
controller: ScrollController(),
child: Column(children: [
const SizedBox(height: 13),
_buildInputTop(),
@@ -64,7 +64,7 @@ class _ConvertPageState extends State<ConvertPage>
showSnackBar(context, Text('Error: \n$e'));
}
},
tooltip: 'convert',
tooltip: s.convert,
child: const Icon(Icons.send),
),
);
@@ -82,20 +82,29 @@ class _ConvertPageState extends State<ConvertPage>
case 3:
return Uri.decodeFull(text);
default:
return 'Unknown';
return s.unkownConvertMode;
}
}
Widget _buildInputTop() {
return SizedBox(
height: _media.size.height * 0.33,
child: _buildInput(_textEditingController),
child: buildInput(context, _textEditingController,
onSubmitted: (_) => _textEditingControllerResult.text = doConvert()),
);
}
Widget _buildTypeOption() {
return Card(
child: ExpansionTile(
final decode = s.decode;
final encode = s.encode;
final List<String> typeOption = [
'Base64 $decode',
'Base64 $encode',
'URL $encode',
'URL $decode'
];
return RoundRectCard(
ExpansionTile(
tilePadding: const EdgeInsets.only(left: 7, right: 27),
childrenPadding: EdgeInsets.zero,
title: Row(
@@ -103,7 +112,7 @@ class _ConvertPageState extends State<ConvertPage>
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: const Icon(Icons.change_circle),
child: Icon(Icons.change_circle, semanticLabel: s.upsideDown),
onPressed: () {
final temp = _textEditingController.text;
_textEditingController.text = _textEditingControllerResult.text;
@@ -113,7 +122,7 @@ class _ConvertPageState extends State<ConvertPage>
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: const Icon(Icons.copy),
child: Icon(Icons.copy, semanticLabel: s.copy),
onPressed: () => FlutterClipboard.copy(
_textEditingControllerResult.text == ''
? ' '
@@ -127,23 +136,23 @@ class _ConvertPageState extends State<ConvertPage>
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_typeOption[_typeOptionIndex],
Text(typeOption[_typeOptionIndex],
textScaleFactor: 1.0,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: primaryColor)),
const Text(
'Current Mode',
Text(
s.currentMode,
textScaleFactor: 1.0,
textAlign: TextAlign.right,
style: TextStyle(fontSize: 9.0, color: Colors.grey),
style: const TextStyle(fontSize: 9.0, color: Colors.grey),
)
],
),
),
children: _typeOption
children: typeOption
.map((e) => ListTile(
title: Text(
e,
@@ -151,7 +160,7 @@ class _ConvertPageState extends State<ConvertPage>
color:
_theme.textTheme.bodyText2!.color!.withAlpha(177)),
),
trailing: _buildRadio(_typeOption.indexOf(e)),
trailing: _buildRadio(typeOption.indexOf(e)),
))
.toList(),
),
@@ -161,20 +170,7 @@ class _ConvertPageState extends State<ConvertPage>
Widget _buildResult() {
return SizedBox(
height: _media.size.height * 0.33,
child: _buildInput(_textEditingControllerResult),
);
}
Widget _buildInput(TextEditingController controller) {
return Card(
child: TextField(
maxLines: 20,
decoration: InputDecoration(
fillColor: Theme.of(context).cardColor,
filled: true,
border: InputBorder.none),
controller: controller,
),
child: buildInput(context, _textEditingControllerResult),
);
}

View File

@@ -14,7 +14,7 @@ class _DebugPageState extends State<DebugPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar:
AppBar(title: const Text('Terminal'), backgroundColor: Colors.black),
AppBar(title: const Text('App log'), backgroundColor: Colors.black),
body: _buildTerminal(context),
backgroundColor: Colors.black,
);

209
lib/view/page/docker.dart Normal file
View File

@@ -0,0 +1,209 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.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/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/two_line_text.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/url_text.dart';
class DockerManagePage extends StatefulWidget {
final ServerPrivateInfo spi;
const DockerManagePage(this.spi, {Key? key}) : super(key: key);
@override
State<DockerManagePage> createState() => _DockerManagePageState();
}
class _DockerManagePageState extends State<DockerManagePage> {
final _docker = locator<DockerProvider>();
final greyTextStyle = const TextStyle(color: Colors.grey);
late S s;
@override
void dispose() {
super.dispose();
_docker.clear();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
void initState() {
super.initState();
final client = locator<ServerProvider>()
.servers
.firstWhere((element) => element.info == widget.spi)
.client;
if (client == null) {
showSnackBar(context, Text(s.noClient));
Navigator.of(context).pop();
return;
}
_docker.init(client);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: 'Docker', down: widget.spi.name),
),
body: _buildMain(),
);
}
Widget _buildMain() {
return Consumer<DockerProvider>(builder: (_, docker, __) {
final running = docker.items;
if (docker.error != null && running == null) {
return SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error,
size: 37,
),
const SizedBox(height: 27),
Text(docker.error!),
const SizedBox(height: 27),
Padding(
padding: const EdgeInsets.all(17),
child: _buildSolution(docker.error!),
)
],
),
);
}
if (running == null) {
_docker.refresh();
return centerLoading;
}
return ListView(
padding: const EdgeInsets.all(7),
children: [
_buildVersion(
docker.edition ?? s.unknown, docker.version ?? s.unknown),
_buildPsItems(running, docker)
].map((e) => RoundRectCard(e)).toList(),
);
});
}
Widget _buildSolution(String err) {
switch (err) {
case 'docker not found':
return UrlText(
text: s.installDockerWithUrl,
replace: s.install,
);
case 'no client':
return Text(s.waitConnection);
case 'invalid version':
return UrlText(
text: s.invalidVersionHelp(issueUrl),
replace: 'Github',
);
default:
return Text(s.unknownError);
}
}
Widget _buildVersion(String edition, String version) {
return Padding(
padding: const EdgeInsets.all(17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(edition), Text(version)],
),
);
}
Widget _buildPsItems(List<DockerPsItem> running, DockerProvider docker) {
return ExpansionTile(
title: Text(s.containerStatus),
subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
children: running.map((item) {
return ListTile(
title: Text(item.image),
subtitle: Text(item.status),
trailing: docker.isBusy
? const CircularProgressIndicator()
: _buildMoreBtn(item.running, item.containerId),
);
}).toList(),
);
}
Widget _buildMoreBtn(bool running, String containerId) {
final item = running ? DockerMenuItems.stop : DockerMenuItems.start;
return DropdownButtonHideUnderline(
child: DropdownButton2(
customButton: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(
Icons.more_vert,
size: 17,
),
),
customItemsHeight: 8,
items: [
DropdownMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
DropdownMenuItem<DropdownBtnItem>(
value: DockerMenuItems.rm,
child: DockerMenuItems.rm.build,
),
],
onChanged: (value) {
final item = value as DropdownBtnItem;
switch (item) {
case DockerMenuItems.rm:
_docker.delete(containerId);
break;
case DockerMenuItems.start:
_docker.start(containerId);
break;
case DockerMenuItems.stop:
_docker.stop(containerId);
break;
}
},
itemHeight: 37,
itemPadding: const EdgeInsets.only(left: 17, right: 17),
dropdownWidth: 133,
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
),
dropdownElevation: 8,
offset: const Offset(0, 8),
),
);
}
String _buildSubtitle(List<DockerPsItem> running) {
final runningCount = running.where((element) => element.running).length;
final stoped = running.length - runningCount;
if (stoped == 0) {
return s.dockerStatusRunningFmt(runningCount);
}
return s.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
}
}

View File

@@ -1,21 +1,30 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter_advanced_drawer/flutter_advanced_drawer.dart';
import 'package:get_it/get_it.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/build_mode.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/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/common.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/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';
@@ -33,28 +42,41 @@ class _MyHomePageState extends State<MyHomePage>
SingleTickerProviderStateMixin,
AfterLayoutMixin,
WidgetsBindingObserver {
final List<String> _tabs = ['Servers', 'En/Decode'];
late final TabController _tabController;
late final ServerProvider _serverProvider;
late final PageController _pageController;
late final AdvancedDrawerController _advancedDrawerController;
late int _selectIndex;
late double _width;
late S s;
@override
void initState() {
super.initState();
_serverProvider = locator<ServerProvider>();
WidgetsBinding.instance?.addObserver(this);
_tabController = TabController(length: _tabs.length, vsync: this);
WidgetsBinding.instance.addObserver(this);
_selectIndex = locator<SettingStore>().launchPage.fetch()!;
_pageController = PageController(initialPage: _selectIndex);
_advancedDrawerController = AdvancedDrawerController();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
_width = MediaQuery.of(context).size.width;
}
@override
void dispose() {
super.dispose();
WidgetsBinding.instance?.removeObserver(this);
WidgetsBinding.instance.removeObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused) {
_serverProvider.setDisconnected();
_serverProvider.stopAutoRefresh();
}
if (state == AppLifecycleState.resumed) {
@@ -64,72 +86,183 @@ class _MyHomePageState extends State<MyHomePage>
@override
Widget build(BuildContext context) {
setTransparentNavigationBar(context);
super.build(context);
return Scaffold(
appBar: AppBar(
title: GestureDetector(
onLongPress: () =>
AppRoute(const DebugPage(), 'Debug Page').go(context),
child: const Text(BuildData.name),
),
bottom: TabBar(
indicatorColor: widget.primaryColor,
tabs: _tabs.map((e) => Tab(text: e)).toList(),
controller: _tabController,
return WillPopScope(
child: _buildMain(context),
onWillPop: () {
if (_advancedDrawerController.value.visible) {
_advancedDrawerController.hideDrawer();
return Future.value(false);
}
return Future.value(true);
});
}
Widget _buildMain(BuildContext context) {
return AdvancedDrawer(
controller: _advancedDrawerController,
animationCurve: Curves.easeInOut,
animationDuration: const Duration(milliseconds: 300),
animateChildDecoration: true,
rtlOpening: false,
childDecoration: const BoxDecoration(
// NOTICE: Uncomment if you want to add shadow behind the page.
// Keep in mind that it may cause animation jerks.
// boxShadow: <BoxShadow>[
// BoxShadow(
// color: Colors.black12,
// blurRadius: 0.0,
// ),
// ],
borderRadius: BorderRadius.all(Radius.circular(16)),
),
drawer: _buildDrawer(),
child: Scaffold(
appBar: AppBar(
title: Text(tabTitleName(context, _selectIndex), style: size18),
actions: [
IconButton(
icon: const Icon(Icons.developer_mode, size: 23),
tooltip: s.debug,
onPressed: () =>
AppRoute(const DebugPage(), 'Debug Page').go(context),
),
],
leading: IconButton(
onPressed: () => _advancedDrawerController.showDrawer(),
icon: ValueListenableBuilder<AdvancedDrawerValue>(
valueListenable: _advancedDrawerController,
builder: (_, value, __) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: Icon(
value.visible ? Icons.clear : Icons.menu,
key: ValueKey<bool>(value.visible),
),
);
},
),
),
),
body: PageView(
physics: const ClampingScrollPhysics(),
controller: _pageController,
onPageChanged: (i) {
FocusScope.of(context).unfocus();
_selectIndex = i;
setState(() {});
},
children: const [ServerPage(), ConvertPage(), PingPage()],
),
bottomNavigationBar: _buildBottom(context),
));
}
Widget _buildItem(int idx, NavigationItem item, bool isSelected) {
bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
final width = _width / tabItems.length;
return AnimatedContainer(
duration: const Duration(milliseconds: 377),
curve: Curves.fastOutSlowIn,
height: 50,
width: isSelected ? width : width - 17,
decoration: BoxDecoration(
color: isSelected
? isDarkMode
? Colors.white12
: Colors.black.withOpacity(0.07)
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(50))),
child: IconButton(
icon: Icon(item.icon),
tooltip: tabTitleName(context, idx),
splashRadius: width / 3.3,
padding: const EdgeInsets.only(left: 17, right: 17),
onPressed: () {
setState(() {
_pageController.animateToPage(idx,
duration: const Duration(milliseconds: 677),
curve: Curves.fastLinearToSlowEaseIn);
});
},
),
drawer: _buildDrawer(),
body: TabBarView(controller: _tabController, children: const [
ServerPage(),
ConvertPage(),
]),
);
}
Widget _buildBottom(BuildContext context) {
return SafeArea(
child: Container(
height: 56,
padding: const EdgeInsets.only(left: 8, top: 4, bottom: 4, right: 8),
width: _width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: tabItems.map((item) {
int itemIndex = tabItems.indexOf(item);
return _buildItem(itemIndex, item, _selectIndex == itemIndex);
}).toList(),
),
));
}
Widget _buildDrawer() {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
return SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
UserAccountsDrawerHeader(
accountName: const Text(BuildData.name),
accountEmail: Text(_buildVersionStr()),
currentAccountPicture: _buildIcon(),
_buildIcon(),
const Text(BuildData.name),
Text(_buildVersionStr()),
SizedBox(
height: MediaQuery.of(context).size.height * 0.07,
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Setting'),
onTap: () => AppRoute(const SettingPage(), 'Setting').go(context),
),
ListTile(
leading: const Icon(Icons.vpn_key),
title: const Text('Private Key'),
onTap: () =>
AppRoute(const StoredPrivateKeysPage(), 'private key list')
.go(context),
),
ListTile(
leading: const Icon(Icons.snippet_folder),
title: const Text('Snippet'),
onTap: () =>
AppRoute(const SnippetListPage(), 'snippet list').go(context),
),
AboutListTile(
icon: const Icon(Icons.text_snippet),
child: const Text('Licences'),
applicationName: BuildData.name,
applicationVersion: _buildVersionStr(),
applicationIcon: _buildIcon(),
aboutBoxChildren: const [
UrlText(
text: '\nMade with ❤️ by $myGithub', replace: 'LollipopKit'),
UrlText(
text:
'\nThanks $rainSunMeGithub for participating in the test.\n\nAll rights reserved.',
replace: 'RainSunMe',
),
],
Padding(
padding: const EdgeInsets.only(left: 29),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.settings),
title: Text(s.setting),
onTap: () =>
AppRoute(const SettingPage(), 'Setting').go(context),
),
ListTile(
leading: const Icon(Icons.vpn_key),
title: Text(s.privateKey),
onTap: () => AppRoute(
const StoredPrivateKeysPage(), 'private key list')
.go(context),
),
ListTile(
leading: const Icon(Icons.download),
title: Text(s.download),
onTap: () =>
AppRoute(const SFTPDownloadedPage(), 'snippet list')
.go(context),
),
ListTile(
leading: const Icon(Icons.snippet_folder),
title: Text(s.snippet),
onTap: () => AppRoute(const SnippetListPage(), 'snippet list')
.go(context),
),
AboutListTile(
icon: const Icon(Icons.text_snippet),
applicationName: BuildData.name,
applicationVersion: _buildVersionStr(),
applicationIcon: _buildIcon(),
aboutBoxChildren: [
UrlText(
text: s.madeWithLove(myGithub), replace: 'LollipopKit'),
UrlText(
text: s.aboutThanks(rainSunMeGithub),
replace: 'RainSunMe',
),
],
child: Text(s.license),
)
],
),
),
],
),
@@ -137,14 +270,28 @@ class _MyHomePageState extends State<MyHomePage>
}
Widget _buildIcon() {
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 60, maxWidth: 60),
child: appIcon,
return Stack(
alignment: Alignment.center,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 53, maxWidth: 53),
child: Container(
color: primaryColor,
)),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 83, maxWidth: 83),
child: appIcon,
)
],
);
}
String _buildVersionStr() {
return 'Ver: 1.0.${BuildData.build}';
var mod = '';
if (BuildData.modifications != 0) {
mod = '(+${BuildData.modifications})';
}
return 'Ver: 1.0.${BuildData.build}$mod';
}
@override

174
lib/view/page/ping.dart Normal file
View File

@@ -0,0 +1,174 @@
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/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';
class PingPage extends StatefulWidget {
const PingPage({Key? key}) : super(key: key);
@override
_PingPageState createState() => _PingPageState();
}
class _PingPageState extends State<PingPage>
with AutomaticKeepAliveClientMixin {
late TextEditingController _textEditingController;
late MediaQueryData _media;
final List<PingResult> _results = [];
late S s;
static const summaryTextStyle = TextStyle(
fontSize: 12,
);
@override
void initState() {
super.initState();
_textEditingController = TextEditingController(text: '');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: GestureDetector(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: Column(children: [
const SizedBox(height: 13),
buildInput(context, _textEditingController,
maxLines: 1, onSubmitted: (_) => doPing()),
_buildControl(),
SizedBox(
width: double.infinity,
height: _media.size.height * 0.6,
child: ListView.builder(
controller: ScrollController(),
itemCount: _results.length,
itemBuilder: (context, index) {
final result = _results[index];
return _buildResultItem(result);
}),
),
])),
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
),
);
}
Widget _buildResultItem(PingResult result) {
final unknown = s.unknown;
final ms = s.ms;
return RoundRectCard(ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
title: Text(result.serverName,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: primaryColor)),
subtitle: Text(
_buildPingSummary(result, unknown, ms),
style: summaryTextStyle,
),
trailing: Text(
'${s.pingAvg}${result.statistic?.avg?.toStringAsFixed(2) ?? s.unknown} $ms',
style: TextStyle(fontSize: 14, color: primaryColor)),
));
}
String _buildPingSummary(PingResult result, String unknown, String ms) {
final ip = result.ip ?? unknown;
if (result.results == null || result.results!.isEmpty) {
return '$ip - ${s.noResult}';
}
final ttl = result.results?.first.ttl ?? unknown;
final loss = result.statistic?.loss ?? unknown;
final min = result.statistic?.min ?? unknown;
final max = result.statistic?.max ?? unknown;
return '$ip\n${s.ttl}: $ttl, ${s.loss}: $loss%\n${s.min}: $min $ms, ${s.max}: $max $ms';
}
Future<void> doPing() async {
_results.clear();
final target = _textEditingController.text.trim();
if (target.isEmpty) {
showSnackBar(context, Text(s.pingInputIP));
return;
}
await Future.wait(locator<ServerProvider>().servers.map((e) async {
if (e.client == null) {
return;
}
final result = await e.client!.run('ping -c 3 $target').string;
_results.add(PingResult.parse(e.info.name, result));
setState(() {});
}));
}
Widget _buildControl() {
return SizedBox(
height: 57,
child: RoundRectCard(
InkWell(
onTap: () => FocusScope.of(context).unfocus(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Row(
children: [
const Icon(Icons.delete),
const SizedBox(
width: 7,
),
Text(s.clear)
],
),
onPressed: () {
_results.clear();
setState(() {});
},
),
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)),
child: Row(
children: [
const Icon(Icons.play_arrow),
const SizedBox(
width: 7,
),
Text(s.start)
],
),
onPressed: () {
try {
doPing();
} catch (e) {
showSnackBar(context, Text('Error: \n$e'));
}
},
)
],
),
),
),
);
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/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';
@@ -25,6 +27,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
late PrivateKeyProvider _provider;
late Widget loading;
late S s;
@override
void initState() {
@@ -33,12 +36,19 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
loading = const SizedBox();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.info != null
? IconButton(
tooltip: s.delete,
onPressed: () {
_provider.delInfo(widget.info!);
Navigator.of(context).pop();
@@ -52,7 +62,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
TextField(
controller: nameController,
keyboardType: TextInputType.text,
decoration: buildDecoration('Name', icon: Icons.info),
decoration: buildDecoration(s.name, icon: Icons.info),
),
TextField(
controller: keyController,
@@ -61,28 +71,27 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
maxLines: 10,
keyboardType: TextInputType.text,
enableSuggestions: false,
decoration: buildDecoration('Private Key', icon: Icons.vpn_key),
decoration: buildDecoration(s.privateKey, icon: Icons.vpn_key),
),
TextField(
controller: pwdController,
autocorrect: false,
keyboardType: TextInputType.text,
obscureText: true,
decoration: buildDecoration('Password', icon: Icons.password),
decoration: buildDecoration(s.pwd, icon: Icons.password),
),
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
loading
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.save),
tooltip: s.save,
onPressed: () async {
final name = nameController.text;
final key = keyController.text;
final pwd = pwdController.text;
if (name.isEmpty || key.isEmpty || pwd.isEmpty) {
showSnackBar(
context, const Text('Three fields must not be empty.'));
if (name.isEmpty || key.isEmpty) {
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
FocusScope.of(context).unfocus();
@@ -114,6 +123,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
}
Navigator.of(context).pop();
},
child: const Icon(Icons.save),
),
);
}

View File

@@ -3,6 +3,9 @@ import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/provider/private_key.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/private_key/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -15,11 +18,19 @@ class StoredPrivateKeysPage extends StatefulWidget {
class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
final _textStyle = TextStyle(color: primaryColor);
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Private Keys'),
title: Text(s.privateKey, style: size18),
),
body: Consumer<PrivateKeyProvider>(
builder: (_, key, __) {
@@ -29,27 +40,30 @@ class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
itemCount: key.infos.length,
itemExtent: 57,
itemBuilder: (context, idx) {
return RoundRectCard(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(
'Edit',
style: _textStyle,
))
],
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,
style: _textStyle,
))
],
),
));
})
: const Center(child: Text('No saved private keys.'));
: Center(child: Text(s.noSavedPrivateKey));
},
),
floatingActionButton: FloatingActionButton(

View File

@@ -1,11 +1,15 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/numx.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/icon/linux_icons.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
const style11 = TextStyle(fontSize: 11);
@@ -23,11 +27,13 @@ class ServerDetailPage extends StatefulWidget {
class _ServerDetailPageState extends State<ServerDetailPage>
with SingleTickerProviderStateMixin {
late MediaQueryData _media;
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
}
@override
@@ -41,10 +47,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildMainPage(ServerInfo si) {
return Scaffold(
appBar: AppBar(
title: Text(si.info.name),
title: Text(si.info.name, style: size18),
),
body: ListView(
padding: const EdgeInsets.all(17),
padding: const EdgeInsets.all(13),
children: [
_buildLinuxIcon(si.status.sysVer),
SizedBox(height: _media.size.height * 0.03),
@@ -68,41 +74,44 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildCPUView(ServerStatus ss) {
return RoundRectCard(
SizedBox(
height: 12 * ss.cpu2Status.coresCount + 67,
child: Column(children: [
SizedBox(
height: _media.size.height * 0.02,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%',
style: const TextStyle(fontSize: 27),
textScaleFactor: 1.0,
),
Row(
children: [
_buildCPUTimePercent(ss.cpu2Status.user, 'user'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildCPUTimePercent(ss.cpu2Status.sys, 'sys'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildCPUTimePercent(ss.cpu2Status.nice, 'nice'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildCPUTimePercent(ss.cpu2Status.idle, 'idle')
],
)
],
),
_buildCPUProgress(ss)
]),
Padding(
padding: roundRectCardPadding,
child: SizedBox(
height: 12 * ss.cpu2Status.coresCount + 67,
child: Column(children: [
SizedBox(
height: _media.size.height * 0.02,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%',
style: const TextStyle(fontSize: 27),
textScaleFactor: 1.0,
),
Row(
children: [
_buildCPUTimePercent(ss.cpu2Status.user, 'user'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildCPUTimePercent(ss.cpu2Status.sys, 'sys'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildCPUTimePercent(ss.cpu2Status.nice, 'nice'),
SizedBox(
width: _media.size.width * 0.03,
),
_buildCPUTimePercent(ss.cpu2Status.idle, 'idle')
],
)
],
),
_buildCPUProgress(ss)
]),
),
),
);
}
@@ -114,7 +123,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
percent.toStringAsFixed(1) + '%',
'${percent.toStringAsFixed(1)}%',
style: const TextStyle(fontSize: 13),
textScaleFactor: 1.0,
),
@@ -158,7 +167,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildUpTimeAndSys(ServerStatus ss) {
return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 13),
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -173,65 +182,56 @@ class _ServerDetailPageState extends State<ServerDetailPage>
));
}
String convertMB(int mb) {
const suffix = ['MB', 'GB', 'TB'];
double value = mb.toDouble();
int squareTimes = 0;
for (; value / 1024 > 1 && squareTimes < 3; squareTimes++) {
value /= 1024;
}
var finalValue = value.toStringAsFixed(1);
if (finalValue.endsWith('.0')) {
finalValue = finalValue.replaceFirst('.0', '');
}
return '$finalValue ${suffix[squareTimes]}';
}
Widget _buildMemView(ServerStatus ss) {
final pColor = primaryColor;
final used = ss.memory.used / ss.memory.total;
final width = _media.size.width - 17 * 2 - 17 * 2;
return RoundRectCard(SizedBox(
height: 47,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildMemExplain(convertMB(ss.memory.used), pColor),
_buildMemExplain(
convertMB(ss.memory.cache), pColor.withAlpha(77)),
_buildMemExplain(convertMB(ss.memory.total - ss.memory.used),
progressColor.resolve(context))
],
),
const SizedBox(
height: 7,
),
Row(
children: [
SizedBox(
width: width * used,
const mb = 1024 * 1024;
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: SizedBox(
height: 47,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildMemExplain((ss.memory.used * mb).convertBytes, pColor),
_buildMemExplain(
(ss.memory.cache * mb).convertBytes, pColor.withAlpha(77)),
_buildMemExplain(
((ss.memory.total - ss.memory.used) * mb).convertBytes,
progressColor.resolve(context))
],
),
const SizedBox(
height: 7,
),
Row(
children: [
SizedBox(
width: width * used,
child: LinearProgressIndicator(
value: 1,
color: pColor,
)),
SizedBox(
width: width * (1 - used),
child: LinearProgressIndicator(
value: 1,
color: pColor,
)),
SizedBox(
width: width * (1 - used),
child: LinearProgressIndicator(
// memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
value: ss.memory.total == 1
? 0
: ss.memory.cache / (ss.memory.total - ss.memory.used),
backgroundColor: progressColor.resolve(context),
color: pColor.withAlpha(77),
),
)
],
)
],
// memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
value: ss.memory.total == 1
? 0
: ss.memory.cache / (ss.memory.total - ss.memory.used),
backgroundColor: progressColor.resolve(context),
color: pColor.withAlpha(77),
),
)
],
)
],
),
),
));
}
@@ -265,7 +265,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
return RoundRectCard(SizedBox(
height: 27 * clone.length + 25,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 13),
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
physics: const NeverScrollableScrollPhysics(),
itemCount: clone.length,
itemBuilder: (_, idx) {
@@ -300,9 +300,19 @@ class _ServerDetailPageState extends State<ServerDetailPage>
height: 7,
)
];
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
if (ns.devices.isEmpty) {
children.add(Center(
child: Text(
s.noInterface,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
));
} else {
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
}
return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 7),
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
child: Column(
children: children,
),

View File

@@ -8,6 +8,9 @@ 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';
@@ -31,10 +34,12 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
late ServerProvider _serverProvider;
late S s;
bool usePublicKey = false;
int _pubKeyIndex = -1;
late PrivateKeyInfo _keyInfo;
PrivateKeyInfo? _keyInfo;
@override
void initState() {
@@ -42,33 +47,35 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_serverProvider = locator<ServerProvider>();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.spi != null
? IconButton(
onPressed: () {
showRoundDialog(
context,
'Attention',
Text(
'Are you sure to delete server [${widget.spi!.name}]'),
[
TextButton(
onPressed: () {
_serverProvider.delServer(widget.spi!);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: const Text(
'Yes',
style: TextStyle(color: Colors.red),
)),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('No'))
]);
showRoundDialog(context, s.attention,
Text(s.sureToDeleteServer(widget.spi!.name)), [
TextButton(
onPressed: () {
_serverProvider.delServer(widget.spi!);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel))
]);
},
icon: const Icon(Icons.delete))
: const SizedBox()
@@ -82,21 +89,21 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
TextField(
controller: nameController,
keyboardType: TextInputType.text,
decoration:
buildDecoration('Name', icon: Icons.info, hint: 'Example'),
decoration: buildDecoration(s.name,
icon: Icons.info, hint: s.exampleName),
),
TextField(
controller: ipController,
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration('Host',
decoration: buildDecoration(s.host,
icon: Icons.storage, hint: 'example.com'),
),
TextField(
controller: portController,
keyboardType: TextInputType.number,
decoration: buildDecoration('Port',
decoration: buildDecoration(s.port,
icon: Icons.format_list_numbered, hint: '22'),
),
TextField(
@@ -104,13 +111,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: false,
decoration: buildDecoration('User',
decoration: buildDecoration(s.user,
icon: Icons.account_box, hint: 'root'),
),
const SizedBox(height: 7),
Row(
children: [
const Text('Public Key Auth'),
Text(s.keyAuth),
Switch(
value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val)),
@@ -121,8 +128,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: passwordController,
obscureText: true,
keyboardType: TextInputType.text,
decoration: buildDecoration('Pwd',
icon: Icons.password, hint: 'Password'),
decoration: buildDecoration(s.pwd,
icon: Icons.password, hint: s.pwd),
onSubmitted: (_) => {},
)
: const SizedBox(),
@@ -142,7 +149,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
)
.toList();
tiles.add(ListTile(
title: const Text('Add a Private Key'),
title: Text(s.addPrivateKey),
contentPadding: EdgeInsets.zero,
trailing: IconButton(
icon: const Icon(Icons.add),
@@ -156,9 +163,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
iconColor: primaryColor,
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: const Text(
'Choose Key',
style: TextStyle(fontSize: 14),
title: Text(
s.choosePrivateKey,
style: const TextStyle(fontSize: 14),
),
children: tiles,
);
@@ -171,15 +178,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
child: const Icon(Icons.send),
onPressed: () {
if (ipController.text == '') {
showSnackBar(context, const Text('Please enter host.'));
showSnackBar(context, Text(s.plzEnterHost));
return;
}
if (!usePublicKey && passwordController.text == '') {
showSnackBar(context, const Text('Please enter password.'));
showSnackBar(context, Text(s.plzEnterPwd));
return;
}
if (usePublicKey && _pubKeyIndex == -1) {
showSnackBar(context, const Text('Please select a private key.'));
showSnackBar(context, Text(s.plzSelectKey));
return;
}
if (usernameController.text == '') {
@@ -188,10 +195,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
if (portController.text == '') {
portController.text = '22';
}
if (widget.spi != null && widget.spi!.pubKeyId != null) {
_keyInfo ??= locator<PrivateKeyStore>().get(widget.spi!.pubKeyId!);
}
final authorization = usePublicKey
? {
"privateKey": _keyInfo.privateKey,
"passphrase": _keyInfo.password
"privateKey": _keyInfo!.privateKey,
"passphrase": _keyInfo!.password
}
: passwordController.text;
final spi = ServerPrivateInfo(
@@ -200,7 +212,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
port: int.parse(portController.text),
user: usernameController.text,
authorization: authorization,
pubKeyId: usePublicKey ? _keyInfo.id : null);
pubKeyId: usePublicKey ? _keyInfo!.id : null);
if (widget.spi == null) {
_serverProvider.addServer(spi);

View File

@@ -1,20 +1,29 @@
import 'package:after_layout/after_layout.dart';
import 'package:circle_chart/circle_chart.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/app/menu_item.dart';
import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/apt.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/list.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
class ServerPage extends StatefulWidget {
const ServerPage({Key? key}) : super(key: key);
@@ -31,6 +40,7 @@ class _ServerPageState extends State<ServerPage>
late RefreshController _refreshController;
late ServerProvider _serverProvider;
late S s;
@override
void initState() {
@@ -45,6 +55,7 @@ class _ServerPageState extends State<ServerPage>
_media = MediaQuery.of(context);
_theme = Theme.of(context);
_primaryColor = primaryColor;
s = S.of(context);
}
@override
@@ -54,22 +65,27 @@ class _ServerPageState extends State<ServerPage>
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return const Center(
return Center(
child: Text(
'There is no server.\nClick the fab to add one.',
s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: Column(
children: [
const SizedBox(height: 13),
...pro.servers.map((e) => _buildEachServerCard(e)),
SizedBox(height: _media.padding.bottom),
],
));
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: autoUpdate
@@ -86,7 +102,7 @@ class _ServerPageState extends State<ServerPage>
onPressed: () =>
AppRoute(const ServerEditPage(), 'Add server info page')
.go(context),
tooltip: 'add a server',
tooltip: s.addAServer,
heroTag: 'server page fab',
child: const Icon(Icons.add),
),
@@ -94,8 +110,8 @@ class _ServerPageState extends State<ServerPage>
}
Widget _buildEachServerCard(ServerInfo si) {
return Card(
child: InkWell(
return RoundRectCard(
InkWell(
onLongPress: () => AppRoute(
ServerEditPage(
spi: si.info,
@@ -103,12 +119,9 @@ class _ServerPageState extends State<ServerPage>
'Edit server info page')
.go(context),
child: Padding(
padding: const EdgeInsets.all(13),
child: SizedBox(
height: _media.size.height * 0.147,
child: _buildRealServerCard(
si.status, si.info.name, si.connectionState)),
),
padding: const EdgeInsets.all(13),
child: _buildRealServerCard(
si.status, si.info.name, si.connectionState, si.info)),
onTap: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'),
'server detail page')
.go(context),
@@ -116,8 +129,8 @@ class _ServerPageState extends State<ServerPage>
);
}
Widget _buildRealServerCard(
ServerStatus ss, String serverName, ServerConnectionState cs) {
Widget _buildRealServerCard(ServerStatus ss, String serverName,
ServerConnectionState cs, ServerPrivateInfo spi) {
final rootDisk =
ss.disk.firstWhere((element) => element.mountLocation == '/');
@@ -131,100 +144,211 @@ class _ServerPageState extends State<ServerPage>
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
serverName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
textScaleFactor: 1.0,
),
hasError
? ConstrainedBox(
constraints: BoxConstraints(
maxWidth: _media.size.width * 0.57, maxHeight: 17),
child: Marquee(
accelerationDuration: const Duration(seconds: 3),
accelerationCurve: Curves.linear,
decelerationDuration: const Duration(seconds: 3),
decelerationCurve: Curves.linear,
text: topRightStr,
textScaleFactor: 1.0,
style: style),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
serverName,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 12),
textScaleFactor: 1.0,
),
const Icon(
Icons.keyboard_arrow_right,
size: 17,
color: Colors.grey,
)
: Text(topRightStr, style: style, textScaleFactor: 1.0),
],
],
),
Row(
children: [
hasError
? ConstrainedBox(
constraints: BoxConstraints(
maxWidth: _media.size.width * 0.57,
maxHeight: 15),
child: Marquee(
accelerationDuration: const Duration(seconds: 3),
accelerationCurve: Curves.linear,
decelerationDuration: const Duration(seconds: 3),
decelerationCurve: Curves.linear,
text: topRightStr,
textScaleFactor: 1.0,
style: style),
)
: Text(topRightStr, style: style, textScaleFactor: 1.0),
_buildMoreBtn(spi),
],
)
],
),
),
const SizedBox(
height: 17,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildPercentCircle(ss.cpu2Status.usedPercent(), 'CPU'),
_buildPercentCircle(ss.memory.used / ss.memory.total * 100, 'Mem'),
_buildIOData('Net', 'Conn:\n' + ss.tcp.maxConn.toString(),
'Fail:\n' + ss.tcp.fail.toString()),
_buildIOData('Disk', 'Total:\n' + rootDisk.size,
'Used:\n' + rootDisk.usedPercent.toString() + '%')
_buildPercentCircle(ss.cpu2Status.usedPercent()),
_buildPercentCircle(ss.memory.used / ss.memory.total * 100),
_buildIOData('Conn:\n${ss.tcp.maxConn}', 'Fail:\n${ss.tcp.fail}'),
_buildIOData(
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
],
),
const SizedBox(height: 13),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildExplainText('CPU'),
_buildExplainText('Mem'),
_buildExplainText('Net'),
_buildExplainText('Disk'),
],
),
const SizedBox(height: 3),
],
);
}
Widget _buildMoreBtn(ServerPrivateInfo spi) {
return DropdownButtonHideUnderline(
child: DropdownButton2(
customButton: const Padding(
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 1.7),
child: Icon(
Icons.more_vert,
size: 17,
),
),
customItemsIndexes: [ServerTabMenuItems.firstItems.length],
customItemsHeight: 8,
items: [
...ServerTabMenuItems.firstItems.map(
(item) => DropdownMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
),
const DropdownMenuItem<Divider>(enabled: false, child: Divider()),
...ServerTabMenuItems.secondItems.map(
(item) => DropdownMenuItem<DropdownBtnItem>(
value: item,
child: item.build,
),
),
],
onChanged: (value) {
final item = value as DropdownBtnItem;
switch (item) {
case ServerTabMenuItems.apt:
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
break;
case ServerTabMenuItems.sftp:
AppRoute(SFTPPage(spi), 'SFTP').go(context);
break;
case ServerTabMenuItems.snippet:
AppRoute(
SnippetListPage(
spi: spi,
),
'snippet list')
.go(context);
break;
case ServerTabMenuItems.edit:
AppRoute(
ServerEditPage(
spi: spi,
),
'Edit server info page')
.go(context);
break;
case ServerTabMenuItems.docker:
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
break;
}
},
itemHeight: 37,
itemPadding: const EdgeInsets.only(left: 17, right: 17),
dropdownWidth: 160,
dropdownPadding: const EdgeInsets.symmetric(vertical: 7),
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
),
dropdownElevation: 8,
offset: const Offset(0, 8),
),
);
}
Widget _buildExplainText(String text) {
return SizedBox(
width: _media.size.width * 0.2,
child: Text(
text,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
textScaleFactor: 1.0,
),
);
}
String getTopRightStr(ServerConnectionState cs, String temp, String upTime,
String? failedInfo) {
switch (cs) {
case ServerConnectionState.disconnected:
return 'Disconnected';
return s.disconnected;
case ServerConnectionState.connected:
return temp == '' ? (upTime == '' ? 'Loading...' : upTime) : temp;
if (temp == '') {
if (upTime == '') {
return s.serverTabLoading;
} else {
return upTime;
}
} else {
if (upTime == '') {
return temp;
} else {
return '$temp | $upTime';
}
}
case ServerConnectionState.connecting:
return 'Connecting...';
return s.serverTabConnecting;
case ServerConnectionState.failed:
if (failedInfo == null) {
return 'Failed';
return s.serverTabFailed;
}
if (failedInfo.contains('encypted')) {
return 'Please "save" this private key again.';
return s.serverTabPlzSave;
}
return failedInfo;
default:
return 'Unknown State';
return s.serverTabUnkown;
}
}
Widget _buildIOData(String title, String up, String down) {
Widget _buildIOData(String up, String down) {
final statusTextStyle = TextStyle(
fontSize: 9, color: _theme.textTheme.bodyText1!.color!.withAlpha(177));
return SizedBox(
width: _media.size.width * 0.2,
height: _media.size.height * 0.1,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
Column(
children: [
Text(
up,
style: statusTextStyle,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
),
const SizedBox(height: 3),
Text(
down + '\n',
style: statusTextStyle,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
)
],
),
const SizedBox(height: 5),
Text(
title,
style: const TextStyle(fontSize: 12),
up,
style: statusTextStyle,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
),
const SizedBox(height: 3),
Text(
down,
style: statusTextStyle,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
)
@@ -233,42 +357,31 @@ class _ServerPageState extends State<ServerPage>
);
}
Widget _buildPercentCircle(double percent, String title) {
Widget _buildPercentCircle(double percent) {
if (percent <= 0) percent = 0.01;
if (percent >= 100) percent = 99.9;
var size = _media.size.height * 0.15;
return SizedBox(
width: _media.size.width * 0.2,
height: _media.size.height * 0.1,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Stack(
children: [
Stack(
children: [
CircleChart(
progressColor: _primaryColor,
progressNumber: percent,
maxNumber: 100,
width: size,
height: size / 1.5,
),
Positioned.fill(
child: Center(
child: Text(
'${percent.toStringAsFixed(1)}%',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 11),
textScaleFactor: 1.0,
),
),
),
],
Center(
child: CircleChart(
progressColor: _primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 53,
height: 53,
),
),
Text(
title,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
textScaleFactor: 1.0,
Positioned.fill(
child: Center(
child: Text(
'${percent.toStringAsFixed(1)}%',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 11),
textScaleFactor: 1.0,
),
),
),
],
),

View File

@@ -2,11 +2,16 @@ import 'package:flutter/material.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';
@@ -20,15 +25,22 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> {
late SettingStore _store;
late int _selectedColorValue;
late int _launchPageIdx;
double _intervalValue = 0;
late Color priColor;
static const textStyle = TextStyle(fontSize: 14);
late final ServerProvider _serverProvider;
late MediaQueryData _media;
late ThemeData _theme;
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
priColor = primaryColor;
_media = MediaQuery.of(context);
_theme = Theme.of(context);
s = S.of(context);
}
@override
@@ -36,6 +48,7 @@ class _SettingPageState extends State<SettingPage> {
super.initState();
_serverProvider = locator<ServerProvider>();
_store = locator<SettingStore>();
_launchPageIdx = _store.launchPage.fetch()!;
_intervalValue = _store.serverStatusUpdateInterval.fetch()!.toDouble();
}
@@ -43,14 +56,15 @@ class _SettingPageState extends State<SettingPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Setting'),
title: Text(s.setting, style: size18),
),
body: ListView(
padding: const EdgeInsets.all(17),
children: [
_buildAppColorPreview(),
_buildUpdateInterval(),
_buildCheckUpdate()
_buildCheckUpdate(),
_buildLaunchPage()
].map((e) => RoundRectCard(e)).toList(),
),
);
@@ -61,15 +75,15 @@ class _SettingPageState extends State<SettingPage> {
String display;
if (app.newestBuild != null) {
if (app.newestBuild! > BuildData.build) {
display = 'Found: v1.0.${app.newestBuild}, click to update';
display = s.versionHaveUpdate(app.newestBuild!);
} else {
display = 'Current: v1.0.${BuildData.build}is up to date';
display = s.versionUpdated(BuildData.build);
}
} else {
display = 'Current: v1.0.${BuildData.build}';
display = s.versionUnknownUpdate(BuildData.build);
}
return ListTile(
contentPadding: EdgeInsets.zero,
contentPadding: roundRectCardPadding,
trailing: const Icon(Icons.keyboard_arrow_right),
title: Text(
display,
@@ -82,19 +96,19 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildUpdateInterval() {
return ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
textColor: priColor,
title: const Text(
'Server status update interval',
title: Text(
s.updateServerStatusInterval,
style: textStyle,
textAlign: TextAlign.start,
),
subtitle: const Text(
'Will take effect immediately.',
style: TextStyle(color: Colors.grey),
subtitle: Text(
s.willTakEeffectImmediately,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
trailing: Text('${_intervalValue.toInt()} s'),
trailing: Text('${_intervalValue.toInt()} ${s.second}'),
children: [
Slider(
thumbColor: priColor,
@@ -111,16 +125,16 @@ class _SettingPageState extends State<SettingPage> {
_store.serverStatusUpdateInterval.put(val.toInt());
_serverProvider.startAutoRefresh();
},
label: '${_intervalValue.toInt()} seconds',
label: '${_intervalValue.toInt()} ${s.second}',
divisions: 10,
),
const SizedBox(
height: 3,
),
_intervalValue == 0.0
? const Text(
'You set to 0, will not update automatically.\nYou can pull to refresh manually.',
style: TextStyle(color: Colors.grey),
? Text(
s.updateIntervalEqual0,
style: const TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
)
: const SizedBox(),
@@ -134,12 +148,8 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildAppColorPreview() {
return ExpansionTile(
textColor: priColor,
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
children: [
_buildAppColorPicker(priColor),
_buildColorPickerConfirmBtn()
],
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
trailing: ClipOval(
child: Container(
color: priColor,
@@ -147,10 +157,14 @@ class _SettingPageState extends State<SettingPage> {
width: 27,
),
),
title: const Text(
'App primary color',
title: Text(
s.appPrimaryColor,
style: textStyle,
));
),
children: [
_buildAppColorPicker(priColor),
_buildColorPickerConfirmBtn()
]);
}
Widget _buildAppColorPicker(Color selected) {
@@ -171,4 +185,49 @@ class _SettingPageState extends State<SettingPage> {
}),
);
}
Widget _buildLaunchPage() {
return ExpansionTile(
textColor: priColor,
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
title: Text(
s.launchPage,
style: textStyle,
),
trailing: ConstrainedBox(
constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
child: Text(
tabTitleName(context, _launchPageIdx),
style: textStyle,
textAlign: TextAlign.right,
),
),
children: tabs
.map((e) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
tabTitleName(context, tabs.indexOf(e)),
style: TextStyle(
fontSize: 14,
color: _theme.textTheme.bodyText2!.color!.withAlpha(177)),
),
trailing: _buildRadio(tabs.indexOf(e)),
))
.toList(),
);
}
Radio _buildRadio(int index) {
return Radio<int>(
value: index,
groupValue: _launchPageIdx,
onChanged: (int? value) {
setState(() {
_launchPageIdx = value!;
_store.launchPage.put(value);
});
},
);
}
}

View File

@@ -0,0 +1,184 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/colorx.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';
class SFTPDownloadedPage extends StatefulWidget {
const SFTPDownloadedPage({Key? key}) : super(key: key);
@override
State<SFTPDownloadedPage> createState() => _SFTPDownloadedPageState();
}
class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
PathWithPrefix? _path;
String? _prefixPath;
late S s;
late ThemeData _theme;
@override
void initState() {
super.initState();
sftpDownloadDir.then((dir) {
_path = PathWithPrefix(dir.path);
_prefixPath = '${dir.path}/';
setState(() {});
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
_theme = Theme.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(s.download),
actions: [
IconButton(
icon: const Icon(Icons.downloading),
onPressed: () =>
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
.go(context),
)
],
),
body: FadeIn(
key: UniqueKey(),
child: _buildBody(),
),
bottomNavigationBar: SafeArea(
child: _buildPath(),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton(
onPressed: (() {
if (_path!.path == _prefixPath) {
showSnackBar(context, Text(s.alreadyLastDir));
return;
}
_path!.update('..');
setState(() {});
}),
child: const Icon(Icons.keyboard_arrow_left),
),
);
}
Widget _buildPath() {
final color = _theme.scaffoldBackgroundColor;
return Container(
color: color,
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
(_path?.path ?? s.loadingFiles).omitStartStr(
style: TextStyle(
color: color.isBrightColor ? Colors.black : Colors.white),
)
],
),
);
}
Widget _buildBody() {
if (_path == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final dir = Directory(_path!.path);
final files = dir.listSync();
return ListView.builder(
itemCount: files.length,
itemBuilder: (context, index) {
var file = files[index];
var fileName = file.path.split('/').last;
var stat = file.statSync();
var isDir = stat.type == FileSystemEntityType.directory;
return ListTile(
leading: isDir
? const Icon(Icons.folder)
: const Icon(Icons.insert_drive_file),
title: Text(fileName),
subtitle: isDir ? null : Text(stat.size.convertBytes),
trailing: Text(
stat.modified
.toString()
.substring(0, stat.modified.toString().length - 4),
style: grey,
),
onTap: () {
if (!isDir) {
showFileActionDialog(file);
return;
}
_path!.update(fileName);
setState(() {});
},
);
},
);
}
void showFileActionDialog(FileSystemEntity file) {
final fileName = file.path.split('/').last;
showRoundDialog(
context,
s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.delete),
title: Text(s.delete),
onTap: () {
Navigator.of(context).pop();
showRoundDialog(
context, s.sureDelete(fileName), const SizedBox(), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () {
file.deleteSync();
setState(() {});
Navigator.of(context).pop();
},
child: Text(s.ok),
),
]);
},
),
ListTile(
leading: const Icon(Icons.open_in_new),
title: Text(s.open),
onTap: () {
shareFiles(context, [file.absolute.path]);
}),
],
),
[
TextButton(
onPressed: (() => Navigator.of(context).pop()),
child: Text(s.close))
]);
}
}

View File

@@ -0,0 +1,104 @@
import 'package:flutter/material.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';
class SFTPDownloadingPage extends StatefulWidget {
const SFTPDownloadingPage({Key? key}) : super(key: key);
@override
_SFTPDownloadingPageState createState() => _SFTPDownloadingPageState();
}
class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
s.download,
style: size18,
),
),
body: _buildBody(),
);
}
Widget _buildBody() {
return Consumer<SftpDownloadProvider>(builder: (__, pro, _) {
if (pro.status.isEmpty) {
return Center(
child: Text(s.sftpNoDownloadTask),
);
}
return ListView.builder(
padding: const EdgeInsets.all(11),
itemCount: pro.status.length,
itemBuilder: (context, index) {
final status = pro.status[index];
return _buildItem(status);
},
);
});
}
Widget _wrapInCard(SftpDownloadStatus status, String? subtitle,
{Widget? trailing}) {
return RoundRectCard(ListTile(
title: Text(status.fileName),
subtitle: subtitle == null
? null
: Text(
subtitle,
style: grey,
),
trailing: trailing,
));
}
Widget _buildItem(SftpDownloadStatus status) {
if (status.error != null) {
showSnackBar(context, Text(status.error.toString()));
}
switch (status.status) {
case SftpWorkerStatus.finished:
final time = status.spentTime.toString();
return _wrapInCard(status,
'${s.downloadFinished} ${s.spentTime(time == 'null' ? s.unknown : (time.substring(0, time.length - 7)))}',
trailing: IconButton(
onPressed: () => shareFiles(context, [status.item.localPath]),
icon: const Icon(Icons.open_in_new)));
case SftpWorkerStatus.downloading:
return _wrapInCard(
status,
s.downloadStatus((status.progress ?? 0.0).toStringAsFixed(2),
(status.size ?? 0).convertBytes),
trailing:
CircularProgressIndicator(value: (status.progress ?? 0) / 100));
case SftpWorkerStatus.preparing:
return _wrapInCard(status, s.sftpDlPrepare, trailing: loadingIcon);
case SftpWorkerStatus.sshConnectted:
return _wrapInCard(status, s.sftpSSHConnected, trailing: loadingIcon);
default:
return _wrapInCard(status, s.unknown,
trailing: const Icon(
Icons.error,
size: 40,
));
}
}
}

View File

@@ -0,0 +1,431 @@
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_connection_state.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';
class SFTPPage extends StatefulWidget {
final ServerPrivateInfo spi;
const SFTPPage(this.spi, {Key? key}) : super(key: key);
@override
_SFTPPageState createState() => _SFTPPageState();
}
class _SFTPPageState extends State<SFTPPage> {
final SftpBrowserStatus _status = SftpBrowserStatus();
final ScrollController _scrollController = ScrollController();
late MediaQueryData _media;
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
}
@override
void initState() {
super.initState();
_status.spi = widget.spi;
_status.selected = true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
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),
)
]),
body: _buildFileView(),
);
}
Widget get centerCircleLoading => Center(
child: Column(
children: [
SizedBox(
height: _media.size.height * 0.4,
),
const CircularProgressIndicator(),
],
),
);
Widget _buildFileView() {
if (!_status.selected) {
return ListView(
children: [
_buildDestSelector(),
],
);
}
final spi = _status.spi;
final si =
locator<ServerProvider>().servers.firstWhere((s) => s.info == spi);
final client = si.client;
if (client == null ||
si.connectionState != ServerConnectionState.connected) {
return centerCircleLoading;
}
if (_status.files == null) {
_status.path = AbsolutePath('/');
listDir(path: '/', client: client);
return centerCircleLoading;
} else {
return RefreshIndicator(
child: FadeIn(
key: Key(_status.spi!.name + _status.path!.path),
child: ListView.builder(
itemCount: _status.files!.length + 1,
controller: _scrollController,
itemBuilder: (context, index) {
if (index == 0) {
return _buildDestSelector();
}
final file = _status.files![index - 1];
final isDir = file.attr.isDirectory;
return ListTile(
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename),
trailing: Text(
DateTime.fromMillisecondsSinceEpoch(
(file.attr.modifyTime ?? 0) * 1000)
.toString()
.replaceFirst('.000', ''),
style: const TextStyle(color: Colors.grey),
),
subtitle:
isDir ? null : Text((file.attr.size ?? 0).convertBytes),
onTap: () {
if (isDir) {
_status.path?.update(file.filename);
listDir(path: _status.path?.path);
} else {
onItemPress(context, file, true);
}
},
onLongPress: () => onItemPress(context, file, false),
);
},
),
),
onRefresh: () => listDir(path: _status.path?.path));
}
}
void onItemPress(BuildContext context, SftpName file, bool showDownload) {
showRoundDialog(
context,
s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.delete),
title: Text(s.delete),
onTap: () => delete(context, file),
),
ListTile(
leading: const Icon(Icons.edit),
title: Text(s.rename),
onTap: () => rename(context, file),
),
showDownload
? ListTile(
leading: const Icon(Icons.download),
title: Text(s.download),
onTap: () => download(context, file),
)
: const SizedBox()
],
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel))
]);
}
void download(BuildContext context, SftpName name) {
showRoundDialog(context, s.download,
Text('${s.dl2Local(name.filename)}\n${s.keepForeground}'), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel)),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final prePath = _status.path!.path;
final remotePath =
prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
final local = '${(await sftpDownloadDir).path}$remotePath';
final pubKeyId = _status.spi!.pubKeyId;
locator<SftpDownloadProvider>().add(
DownloadItem(_status.spi!, remotePath, local),
key: pubKeyId == null
? null
: locator<PrivateKeyStore>().get(pubKeyId).privateKey);
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))
]);
}
void delete(BuildContext context, SftpName file) {
Navigator.of(context).pop();
showRoundDialog(context, s.attention, Text(s.sureDelete(file.filename)), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
TextButton(
onPressed: () {
_status.client!.remove(file.filename);
Navigator.of(context).pop();
listDir();
},
child: Text(
s.delete,
style: const TextStyle(color: Colors.red),
)),
]);
}
void mkdir(BuildContext context) {
Navigator.of(context).pop();
final textController = TextEditingController();
showRoundDialog(
context,
s.createFolder,
TextField(
controller: textController,
decoration: InputDecoration(
labelText: s.name,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
_status.client!
.mkdir('${_status.path!.path}/${textController.text}');
Navigator.of(context).pop();
listDir();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
]);
}
void newFile(BuildContext context) {
Navigator.of(context).pop();
final textController = TextEditingController();
showRoundDialog(
context,
s.createFile,
TextField(
controller: textController,
decoration: InputDecoration(
labelText: s.name,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
(await _status.client!
.open('${_status.path!.path}/${textController.text}'))
.writeBytes(Uint8List(0));
Navigator.of(context).pop();
listDir();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
]);
}
void rename(BuildContext context, SftpName file) {
Navigator.of(context).pop();
final textController = TextEditingController();
showRoundDialog(
context,
s.rename,
TextField(
controller: textController,
decoration: InputDecoration(
labelText: s.name,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
await _status.client!
.rename(file.filename, textController.text);
Navigator.of(context).pop();
listDir();
},
child: Text(
s.rename,
style: const TextStyle(color: Colors.red),
)),
]);
}
Future<void> listDir({String? path, SSHClient? client}) async {
if (_status.isBusy) {
return;
}
_status.isBusy = true;
if (client != null) {
final sftpc = await client.sftp();
_status.client = sftpc;
}
try {
final fs =
await _status.client!.listdir(path ?? (_status.path?.path ?? '/'));
fs.sort((a, b) => a.filename.compareTo(b.filename));
fs.removeAt(0);
if (mounted) {
setState(() {
_status.files = fs;
_status.isBusy = false;
});
}
} catch (e) {
await showRoundDialog(context, s.error, Text(e.toString()), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
]);
if (_status.path!.undo()) {
await listDir();
}
}
}
Widget _buildDestSelector() {
final str = _status.path?.path;
return ExpansionTile(
title: Text(_status.spi?.name ?? s.chooseDestination),
subtitle: _status.selected
? str!.omitStartStr(style: const TextStyle(color: Colors.grey))
: null,
children: locator<ServerProvider>()
.servers
.map((e) => _buildDestSelectorItem(e.info))
.toList());
}
Widget _buildDestSelectorItem(ServerPrivateInfo spi) {
return ListTile(
title: Text(spi.name),
subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'),
onTap: () {
_status.spi = spi;
_status.selected = true;
_status.path = AbsolutePath('/');
listDir(
client: locator<ServerProvider>()
.servers
.firstWhere((s) => s.info == spi)
.client,
path: '/');
},
);
}
}

View File

@@ -3,6 +3,8 @@ 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';
@@ -21,6 +23,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
final scriptController = TextEditingController();
late SnippetProvider _provider;
late S s;
@override
void initState() {
@@ -28,16 +31,23 @@ class _SnippetEditPageState extends State<SnippetEditPage>
_provider = locator<SnippetProvider>();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.snippet != null
? IconButton(
onPressed: () {
_provider.delInfo(widget.snippet!);
_provider.del(widget.snippet!);
Navigator.of(context).pop();
},
tooltip: s.delete,
icon: const Icon(Icons.delete))
: const SizedBox()
]),
@@ -47,7 +57,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
TextField(
controller: nameController,
keyboardType: TextInputType.text,
decoration: buildDecoration('Name', icon: Icons.info),
decoration: buildDecoration(s.name, icon: Icons.info),
),
TextField(
controller: scriptController,
@@ -56,7 +66,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
maxLines: 10,
keyboardType: TextInputType.text,
enableSuggestions: false,
decoration: buildDecoration('Snippet', icon: Icons.code),
decoration: buildDecoration(s.snippet, icon: Icons.code),
),
],
),
@@ -66,14 +76,14 @@ class _SnippetEditPageState extends State<SnippetEditPage>
final name = nameController.text;
final script = scriptController.text;
if (name.isEmpty || script.isEmpty) {
showSnackBar(context, const Text('Two fields must not be empty.'));
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
final snippet = Snippet(name, script);
if (widget.snippet != null) {
_provider.updateInfo(widget.snippet!, snippet);
_provider.update(widget.snippet!, snippet);
} else {
_provider.addInfo(snippet);
_provider.add(snippet);
}
Navigator.of(context).pop();
},

View File

@@ -1,31 +1,55 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/snippet/edit.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
class SnippetListPage extends StatefulWidget {
const SnippetListPage({Key? key}) : super(key: key);
const SnippetListPage({Key? key, this.spi}) : super(key: key);
final ServerPrivateInfo? spi;
@override
_SnippetListPageState createState() => _SnippetListPageState();
}
class _SnippetListPageState extends State<SnippetListPage> {
int _selectedIndex = 0;
late ServerPrivateInfo _selectedIndex;
final _importFieldController = TextEditingController();
final _exportFieldController = TextEditingController();
final _textStyle = TextStyle(color: primaryColor);
late S s;
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Snippet List'),
title: Text(s.snippet, style: size18),
actions: [
IconButton(
onPressed: () => _showImportExport(),
tooltip: s.importAndExport,
icon: const Icon(Icons.import_export)),
],
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
@@ -36,6 +60,97 @@ class _SnippetListPageState extends State<SnippetListPage> {
);
}
Future<void> _showImportExport() async {
await showRoundDialog(
context,
s.choose,
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(s.import),
leading: const Icon(Icons.download),
onTap: () => _showImportDialog(),
),
ListTile(
title: Text(s.export),
leading: const Icon(Icons.file_upload),
onTap: () => _showExportDialog(),
),
],
),
[]);
}
Future<void> _showExportDialog() async {
Navigator.of(context).pop();
_exportFieldController.text = locator<SnippetProvider>().export;
await showRoundDialog(
context,
s.export,
TextField(
decoration: const InputDecoration(
labelText: 'JSON',
),
maxLines: 3,
controller: _exportFieldController,
),
[
TextButton(
child: Text(s.ok),
onPressed: () => Navigator.pop(context),
),
]);
}
Future<void> _showImportDialog() async {
Navigator.of(context).pop();
await showRoundDialog(
context,
s.import,
TextField(
decoration: InputDecoration(
labelText: s.urlOrJson,
),
maxLines: 2,
controller: _importFieldController,
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async =>
await _import(_importFieldController.text.trim()),
child: const Text('GO'),
)
]);
}
Future<void> _import(String text) async {
if (text.isEmpty) {
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
final snippetProvider = locator<SnippetProvider>();
if (text.startsWith('http')) {
final resp = await Dio().get(text);
if (resp.statusCode != 200) {
showSnackBar(
context, Text(s.httpFailedWithCode(resp.statusCode ?? '-1')));
return;
}
for (final snippet in getSnippetList(resp.data)) {
snippetProvider.add(snippet);
}
} else {
for (final snippet in getSnippetList(text)) {
snippetProvider.add(snippet);
}
}
Navigator.of(context).pop();
}
Widget _buildBody() {
return Consumer<SnippetProvider>(
builder: (_, key, __) {
@@ -45,49 +160,65 @@ class _SnippetListPageState extends State<SnippetListPage> {
itemCount: key.snippets.length,
itemExtent: 57,
itemBuilder: (context, idx) {
return RoundRectCard(Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
key.snippets[idx].name,
textAlign: TextAlign.center,
),
Row(children: [
TextButton(
onPressed: () => AppRoute(
SnippetEditPage(snippet: key.snippets[idx]),
'snippet edit page')
.go(context),
child: Text(
'Edit',
style: _textStyle,
)),
TextButton(
onPressed: () => _showRunDialog(key.snippets[idx]),
child: Text(
'Run',
style: _textStyle,
))
])
],
return RoundRectCard(Padding(
padding: roundRectCardPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
key.snippets[idx].name,
textAlign: TextAlign.center,
),
Row(children: [
TextButton(
onPressed: () => AppRoute(
SnippetEditPage(
snippet: key.snippets[idx]),
'snippet edit page')
.go(context),
child: Text(
s.edit,
style: _textStyle,
)),
TextButton(
onPressed: () {
final snippet = key.snippets[idx];
if (widget.spi == null) {
_showRunDialog(snippet);
return;
}
run(context, snippet);
},
child: Text(
s.run,
style: _textStyle,
))
])
],
),
));
})
: const Center(child: Text('No saved snippets.'));
: Center(child: Text(s.noSavedSnippet));
},
);
}
void _showRunDialog(Snippet snippet) {
showRoundDialog(context, 'Choose destination',
showRoundDialog(context, s.chooseDestination,
Consumer<ServerProvider>(builder: (_, provider, __) {
if (provider.servers.isEmpty) {
return const Text('No server available');
return Text(s.noServerAvailable);
}
_selectedIndex = provider.servers.first.info;
return SizedBox(
height: 111,
child: Stack(children: [
Positioned(
top: 36,
bottom: 36,
left: 0,
right: 0,
child: Container(
height: 37,
decoration: const BoxDecoration(
@@ -95,16 +226,13 @@ class _SnippetListPageState extends State<SnippetListPage> {
color: Colors.black12,
),
),
top: 36,
bottom: 36,
left: 0,
right: 0,
),
ListWheelScrollView.useDelegate(
itemExtent: 37,
diameterRatio: 1.2,
controller: FixedExtentScrollController(initialItem: 0),
onSelectedItemChanged: (idx) => _selectedIndex = idx,
onSelectedItemChanged: (idx) =>
_selectedIndex = provider.servers[idx].info,
physics: const FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) => Center(
@@ -118,22 +246,21 @@ class _SnippetListPageState extends State<SnippetListPage> {
]));
}), [
TextButton(
onPressed: () async {
final result = await locator<ServerProvider>()
.runSnippet(_selectedIndex, snippet);
if (result != null) {
showRoundDialog(context, 'Result',
Text(result, style: const TextStyle(fontSize: 13)), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'))
]);
}
},
child: const Text('Run')),
onPressed: () async => run(context, snippet), child: Text(s.run)),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel)),
]);
}
Future<void> run(BuildContext context, Snippet snippet) async {
final result = await locator<ServerProvider>()
.runSnippet(widget.spi ?? _selectedIndex, snippet);
if (result != null) {
showRoundDialog(context, s.result,
Text(result, style: const TextStyle(fontSize: 13)), [
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.close))
]);
}
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
const centerLoading = Center(child: CircularProgressIndicator());
const centerSizedLoading = SizedBox(
width: 77,
height: 77,
child: Center(
child: CircularProgressIndicator(),
),
);
final loadingIcon = IconButton(onPressed: () {}, icon: centerLoading);

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
/// 渐隐渐显实现
class FadeIn extends StatefulWidget {
final Widget child;
const FadeIn({Key? key, required this.child}) : super(key: key);
@override
_MyFadeInState createState() => _MyFadeInState();
}
class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 377),
);
_animation = Tween(
begin: 0.0,
end: 1.0,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_controller.forward();
return FadeTransition(
opacity: _animation,
child: widget.child,
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
Widget buildInput(BuildContext context, TextEditingController controller,
{int maxLines = 20,
String? hint,
Function(String)? onSubmitted,
bool? obscureText}) {
return RoundRectCard(
TextField(
maxLines: maxLines,
onSubmitted: onSubmitted,
decoration: InputDecoration(
fillColor: Theme.of(context).cardColor,
hintText: hint,
filled: true,
border: InputBorder.none),
controller: controller,
obscureText: obscureText ?? false,
),
);
}

View File

@@ -8,13 +8,10 @@ class RoundRectCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 17),
child: child,
),
margin: const EdgeInsets.symmetric(vertical: 7),
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(17))),
child: child,
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
class TwoLineText extends StatelessWidget {
const TwoLineText({Key? key, required this.up, required this.down})
: super(key: key);
final String up;
final String down;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
up,
style: const TextStyle(fontSize: 15),
),
Text(
down,
style: const TextStyle(fontSize: 11),
)
],
);
}
}

View File

@@ -8,23 +8,25 @@ const regUrl =
class UrlText extends StatelessWidget {
final String text;
final String? replace;
final TextAlign? textAlign;
final TextStyle style;
const UrlText(
{Key? key,
required this.text,
this.replace,
this.textAlign,
this.style = const TextStyle()})
: super(key: key);
List<InlineSpan> _getTextSpans(bool isDarkMode) {
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;
for (Match match in _matches) {
for (Match match in matches) {
final group0 = match.group(0);
if (group0 != null && group0.isNotEmpty) {
if (start != match.start) {
@@ -69,6 +71,7 @@ class UrlText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RichText(
textAlign: textAlign ?? TextAlign.start,
text: TextSpan(children: _getTextSpans(isDarkMode(context))),
);
}

View File

@@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
@@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -6,9 +6,11 @@ import FlutterMacOS
import Foundation
import path_provider_macos
import share_plus_macos
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -2,12 +2,15 @@ PODS:
- FlutterMacOS (1.0.0)
- path_provider_macos (0.0.1):
- FlutterMacOS
- share_plus_macos (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
- share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
@@ -15,14 +18,17 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
path_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
share_plus_macos:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
path_provider_macos: 160cab0d5461f0c0e02995469a98f24bdb9a3f1f
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
COCOAPODS: 1.11.2
COCOAPODS: 1.11.3

View File

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

118
make.dart
View File

@@ -5,6 +5,23 @@ import 'dart:convert';
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';
const macosAppPath = 'build/macos/Build/Products/Release/$appName.app';
var regiOSProjectVer = RegExp(r'CURRENT_PROJECT_VERSION = .+;');
var regiOSMarketVer = RegExp(r'MARKETING_VERSION = .+');
const iOSInfoPlistPath = 'ios/Runner.xcodeproj/project.pbxproj';
const macOSInfoPlistPath = 'macos/Runner.xcodeproj/project.pbxproj';
const skslFileSuffix = '.sksl.json';
const buildFuncs = {
'ios': flutterBuildIOS,
'macos': flutterBuildMacOS,
'android': flutterBuildAndroid,
};
int? build;
Future<int> getGitCommitCount() async {
final result = await Process.run('git', ['log', '--oneline']);
@@ -46,7 +63,7 @@ Future<String> getFlutterVersion() async {
Future<Map<String, dynamic>> getBuildData() async {
final data = {
'name': appName,
'build': await getGitCommitCount(),
'build': build,
'engine': await getFlutterVersion(),
'buildAt': DateTime.now().toString(),
'modifications': await getGitModificationCount(),
@@ -63,27 +80,42 @@ Future<void> updateBuildData() async {
print('Updating BuildData...');
final data = await getBuildData();
print(jsonEncodeWithIndent(data));
const path = 'lib/data/res/build_data.dart';
await writeStaicConfigFile(data, 'BuildData', path);
await writeStaicConfigFile(data, 'BuildData', buildDataFilePath);
}
Future<void> dartFormat() async {
final result = await Process.run('dart', ['format', '.']);
print('\n${result.stdout}');
if (result.exitCode != 0) {
print(result.stderr);
exit(1);
}
}
void flutterRun(String? mode) {
Process.start('flutter', ['run', mode == null ? '' : '--$mode'],
Process.start('flutter', mode == null ? ['run'] : ['run', '--$mode'],
mode: ProcessStartMode.inheritStdio, runInShell: true);
}
Future<void> flutterBuild(String source, String target, bool isAndroid) async {
final startTime = DateTime.now();
final build = await getGitCommitCount();
Future<void> flutterBuild(
String source, String target, String buildType) async {
final args = [
'build',
isAndroid ? 'apk' : 'ipa',
'--target-platform=android-arm64',
'--build-number=$build',
'--build-name=1.0.$build'
buildType,
];
if (!isAndroid) args.removeAt(2);
// No sksl cache for macos
if ('macos' != buildType) {
args.add('--bundle-sksl-path=$buildType$skslFileSuffix');
}
final isAndroid = 'apk' == buildType;
// [--target-platform] only for Android
if (isAndroid) {
args.addAll([
'--target-platform=android-arm64',
'--build-number=$build',
'--build-name=1.0.$build',
]);
}
print('Building with args: ${args.join(' ')}');
final buildResult = await Process.run('flutter', args, runInShell: true);
final exitCode = buildResult.exitCode;
@@ -91,25 +123,49 @@ Future<void> flutterBuild(String source, String target, bool isAndroid) async {
if (exitCode == 0) {
target = target.replaceFirst('build', build.toString());
print('Copying from $source to $target');
await File(source).copy(target);
print('Done.');
if (isAndroid) {
await File(source).copy(target);
} else {
final result = await Process.run('cp', ['-r', source, target]);
if (result.exitCode != 0) {
print(result.stderr);
exit(1);
}
}
print('Done.\n');
} else {
print(buildResult.stderr.toString());
print('\nBuild failed with exit code $exitCode');
exit(exitCode);
}
final endTime = DateTime.now();
print('Spent time: ${endTime.difference(startTime).toString()}');
}
Future<void> flutterBuildIOS() async {
await flutterBuild('./build/ios/iphoneos/ToastTiku.app',
'./release/${appName}_build.app', false);
await changeInfoPlistVersion();
await flutterBuild(
xcarchivePath, './release/${appName}_ios_build.xcarchive', 'ipa');
}
Future<void> flutterBuildMacOS() async {
await changeInfoPlistVersion();
await flutterBuild(
macosAppPath, './release/${appName}_macos_build.app', 'macos');
}
Future<void> flutterBuildAndroid() async {
await flutterBuild('./build/app/outputs/flutter-apk/app-release.apk',
'./release/${appName}_build_Arm64.apk', true);
await flutterBuild(apkPath, './release/${appName}_build_Arm64.apk', 'apk');
}
Future<void> changeInfoPlistVersion() async {
for (final path in [iOSInfoPlistPath, macOSInfoPlistPath]) {
final file = File(path);
final contents = await file.readAsString();
final newContents = contents
.replaceAll(regiOSMarketVer, 'MARKETING_VERSION = 1.0.$build;')
.replaceAll(regiOSProjectVer, 'CURRENT_PROJECT_VERSION = $build;');
await file.writeAsString(newContents);
}
}
void main(List<String> args) async {
@@ -124,15 +180,23 @@ void main(List<String> args) async {
case 'run':
return flutterRun(args.length == 2 ? args[1] : null);
case 'build':
final stopwatch = Stopwatch()..start();
build = await getGitCommitCount();
await updateBuildData();
await dartFormat();
if (args.length > 1) {
await updateBuildData();
if (args[1] == 'android' || args[1] == 'harmony') {
return flutterBuildAndroid();
} else if (args[1] == 'ios') {
return flutterBuildIOS();
final platform = args[1];
if (buildFuncs.keys.contains(platform)) {
await buildFuncs[platform]!();
} else {
print('Unknown platform: $platform');
}
print('unkonwn build arg: ${args[1]}');
return;
}
for (final func in buildFuncs.values) {
await func();
}
print('Build finished in ${stopwatch.elapsed}');
return;
default:
print('Unsupported command: $command');

View File

@@ -14,21 +14,21 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.8"
version: "3.3.0"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
version: "2.3.1"
asn1lib:
dependency: transitive
description:
name: asn1lib
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "1.1.0"
async:
dependency: transitive
description:
@@ -62,7 +62,7 @@ packages:
description:
path: "."
ref: main
resolved-ref: "36a46aaa41690aac96fa808a6e75841464007a3b"
resolved-ref: "01eb9bcc7f1a1690381caeedb476ea98c5295d55"
url: "https://github.com/LollipopKit/circle_chart"
source: git
version: "0.0.3"
@@ -86,7 +86,7 @@ packages:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
version: "1.16.0"
convert:
dependency: transitive
description:
@@ -99,17 +99,17 @@ packages:
description:
path: "."
ref: master
resolved-ref: dcb1f6084242a7b4b8e4edd4b180a42ad7f595ca
resolved-ref: "585759048345ab7defdfd9cae3a6c76dc451fe51"
url: "https://github.com/Countly/countly-sdk-flutter-bridge.git"
source: git
version: "20.11.4"
version: "21.11.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.0.2"
dart_console:
dependency: transitive
description:
@@ -123,28 +123,42 @@ packages:
name: dartssh2
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0-pre"
version: "2.6.1"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.4"
version: "4.0.6"
dropdown_button2:
dependency: "direct main"
description:
name: dropdown_button2
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
easy_isolate:
dependency: "direct main"
description:
name: easy_isolate
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
extended_image:
dependency: "direct main"
description:
name: extended_image
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.3"
version: "6.2.0"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
version: "3.3.0"
fading_edge_scrollview:
dependency: transitive
description:
@@ -158,7 +172,7 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0"
ffi:
dependency: transitive
description:
@@ -178,13 +192,25 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_advanced_drawer:
dependency: "direct main"
description:
name: flutter_advanced_drawer
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.2"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "2.0.1"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_material_color_picker:
dependency: "direct main"
description:
@@ -198,14 +224,14 @@ packages:
name: flutter_native_splash
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.3"
version: "2.1.6"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.0.6"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -229,7 +255,7 @@ packages:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.1.0"
hive_flutter:
dependency: "direct main"
description:
@@ -264,21 +290,35 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "3.1.3"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
version: "0.6.4"
lint:
dependency: transitive
description:
name: lint
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "2.0.0"
logging:
dependency: "direct main"
description:
@@ -292,7 +332,7 @@ packages:
name: marquee
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "2.2.1"
matcher:
dependency: transitive
description:
@@ -300,6 +340,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
meta:
dependency: transitive
description:
@@ -307,6 +354,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
nested:
dependency: transitive
description:
@@ -320,56 +374,56 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
version: "1.8.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.0.10"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
version: "2.0.14"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
version: "2.0.9"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "2.1.6"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.6"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.4"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.6"
pedantic:
dependency: transitive
description:
@@ -383,14 +437,14 @@ packages:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
version: "5.0.0"
pinenacl:
dependency: transitive
description:
name: pinenacl
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
version: "0.3.4"
platform:
dependency: transitive
description:
@@ -411,7 +465,7 @@ packages:
name: pointycastle
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.0"
version: "3.6.0"
process:
dependency: transitive
description:
@@ -439,7 +493,49 @@ packages:
name: r_upgrade
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.7+2"
version: "0.3.8+1"
share_plus:
dependency: "direct main"
description:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.4"
share_plus_linux:
dependency: transitive
description:
name: share_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_macos:
dependency: transitive
description:
name: share_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
share_plus_web:
dependency: transitive
description:
name: share_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_windows:
dependency: transitive
description:
name: share_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
@@ -451,7 +547,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
stack_trace:
dependency: transitive
description:
@@ -486,7 +582,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.4.9"
typed_data:
dependency: transitive
description:
@@ -507,35 +603,35 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
version: "6.1.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.13"
version: "6.0.17"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.13"
version: "6.0.16"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.1"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -549,49 +645,42 @@ packages:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.0.11"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
validators:
dependency: transitive
description:
name: validators
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.0.1"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "2.1.2"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.3"
version: "2.5.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.0+1"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.3.1"
version: "5.4.1"
yaml:
dependency: transitive
description:
@@ -600,5 +689,5 @@ packages:
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.15.1 <3.0.0"
flutter: ">=2.5.0"
dart: ">=2.17.0 <3.0.0"
flutter: ">=3.0.0"

View File

@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=2.17.0 <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -29,34 +29,41 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
provider: ^6.0.0
get_it: ^7.2.0
hive: ^2.0.0
hive_flutter: ^1.0.0
dio: ^4.0.0
after_layout: ^1.1.0
extended_image: ^5.1.3
extended_image: ^6.0.3
url_launcher: ^6.0.9
countly_flutter:
git:
git:
url: https://github.com/Countly/countly-sdk-flutter-bridge.git
ref: master
dartssh2: ^2.3.0-pre
dartssh2: ^2.3.1-pre
logging: ^1.0.2
flutter_material_color_picker: ^1.1.0+2
circle_chart:
git:
circle_chart:
git:
url: https://github.com/LollipopKit/circle_chart
ref: main
# path: ../circle_chart
clipboard: ^0.1.3
r_upgrade: ^0.3.6
pull_to_refresh: ^2.0.0
marquee: ^2.2.0
dropdown_button2: ^1.1.1
flutter_advanced_drawer: ^1.3.0
path_provider: ^2.0.9
easy_isolate: ^1.3.0
share_plus: ^4.0.4
intl: ^0.17.0
dev_dependencies:
flutter_native_splash: ^1.2.4
flutter_native_splash: ^2.1.6
flutter_test:
sdk: flutter
@@ -65,11 +72,10 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
@@ -90,13 +96,10 @@ flutter:
- assets/linux/opensuse.png
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
@@ -116,7 +119,6 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
flutter_native_splash:
# This package generates native code to customize Flutter's default white native splash screen
# with background color and splash image.
@@ -124,17 +126,14 @@ flutter_native_splash:
# flutter pub run flutter_native_splash:create
# To restore Flutter's default white splash screen, run the following command in the terminal:
# flutter pub run flutter_native_splash:remove
# color or background_image is the only required parameter. Use color to set the background
# of your splash screen to a solid color. Use background_image to set the background of your
# splash screen to a png image. This is useful for gradients. The image will be stretch to the
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
color: "#ffffff"
#background_image: "assets/background.png"
# Optional parameters are listed below. To enable a parameter, uncomment the line by removing
# the leading # character.
# The image parameter allows you to specify an image used in the splash screen. It must be a
# png file and should be sized for 4x pixel density.
image: assets/app_icon.png
@@ -144,16 +143,14 @@ flutter_native_splash:
# parameters from above. If the image_dark parameter is specified, color_dark or
# background_image_dark must be specified. color_dark and background_image_dark cannot both be
# set.
#color_dark: "#042a49"
color_dark: "#121212"
#background_image_dark: "assets/dark-background.png"
#image_dark: assets/splash-invert.png
# The android, ios and web parameters can be used to disable generating a splash screen on a given
# platform.
#android: false
#ios: false
#web: false
# The position of the splash image can be set with android_gravity, ios_content_mode, and
# web_image_mode parameters. All default to center.
#
@@ -171,7 +168,6 @@ flutter_native_splash:
#
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
#web_image_mode: center
# To hide the notification bar, use the fullscreen parameter. Has no affect in web since web
# has no notification bar. Defaults to false.
# NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
@@ -179,13 +175,13 @@ flutter_native_splash:
# WidgetsFlutterBinding.ensureInitialized();
# SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
#fullscreen: true
# If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
# with the info_plist_files parameter. Remove only the # characters in the three lines below,
# do not remove any spaces:
#info_plist_files:
# - 'ios/Runner/Info-Debug.plist'
# - 'ios/Runner/Info-Release.plist'
# To enable support for Android 12, set the following parameter to true. Defaults to false.
#android12: true
#android12: true
flutter_intl:
enabled: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Some files were not shown because too many files have changed in this diff Show More