Compare commits

..

93 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
Junyuan Feng
db3ec38a0f Change app name 2022-01-11 13:34:15 +08:00
Junyuan Feng
a58119ef54 Optimize load speed 2022-01-11 13:21:52 +08:00
Junyuan Feng
340a7641e6 Solve ssh connect slow 2022-01-11 12:19:17 +08:00
Junyuan Feng
4d227c3f00 Fill macOS data 2022-01-10 17:04:18 +08:00
Junyuan Feng
195e188d0c Fix issue caused by flutter_staggered_animations 2022-01-10 16:00:11 +08:00
Junyuan Feng
692f3bcdbc Isolate SSHKeyPair process to avoid run stuck 2022-01-10 12:10:56 +08:00
Junyuan Feng
516cec5565 change default macos window size 2022-01-09 23:11:51 +08:00
Junyuan Feng
5a706313f5 use 9/16 to fit layout 2022-01-09 22:22:31 +08:00
Junyuan Feng
937f2fb38f fix server tab circle chart size 2022-01-09 21:14:47 +08:00
Junyuan Feng
7e36c06542 Solve launches too slow 2022-01-09 19:21:42 +08:00
Junyuan Feng
317936c521 Change url 2022-01-09 18:18:06 +08:00
Junyuan Feng
a2ed889183 migrate to darssh2 2022-01-09 18:06:39 +08:00
Junyuan Feng
3fc43d06e9 Add padding for bottom little white tile 2022-01-03 15:03:38 +08:00
Junyuan Feng
b922428c40 Fix mem progress display percent. 2022-01-02 19:26:00 +08:00
Junyuan Feng
e08f37fedc Improve.
- make the refresh interval setting makes effect immediately.
- auto stop/restart status update when app goto background/resume
2022-01-02 19:06:26 +08:00
Junyuan Feng
7fb8c88ab8 detail page display memory exact value 2021-12-31 18:51:33 +08:00
Junyuan Feng
f480c49f1f scroll server connection failed info 2021-12-31 17:55:07 +08:00
Junyuan Feng
f7558d6beb can manually refresh when updateInterval==0 2021-12-31 17:44:17 +08:00
LollipopKit
de1e970108 Add r/w permission 2021-11-21 19:45:42 +08:00
LollipopKit
9ef59f4c12 Fix: app update issue on MIUI 2021-11-21 19:36:07 +08:00
LollipopKit
89ef2cb95c Will display the exception of Server Connection 2021-11-08 19:13:24 +08:00
LollipopKit
e0fb591dea Simply implement snippet running. 2021-11-06 14:05:03 +08:00
LollipopKit
7c34530821 Fix update things 2021-11-02 20:58:50 +08:00
LollipopKit
72c1901989 Add check update btn in setting 2021-11-02 20:31:44 +08:00
LollipopKit
ff76c6c539 Allow keep data on uninstalling 2021-11-02 19:49:29 +08:00
LollipopKit
d38bad7802 Update memList to Memory 2021-11-02 19:21:04 +08:00
LollipopKit
9e73dd07ca Optimized view logic 2021-11-02 15:48:24 +08:00
LollipopKit
3105552eae Init analysis only release mode 2021-11-02 15:48:04 +08:00
LollipopKit
ad05b296f6 Redesign net speed part 2021-11-02 15:29:17 +08:00
LollipopKit
f0a8941b59 Optimized net speed view 2021-11-01 22:19:32 +08:00
LollipopKit
fbc8f9598d Support to get net speed 2021-11-01 21:29:12 +08:00
LollipopKit
e7d87b40b8 Porgress use dynamic color 2021-11-01 15:26:29 +08:00
LollipopKit
1cd69c8f44 Fix: logical error when connecting to the server 2021-10-31 22:01:36 +08:00
LollipopKit
6e3fca32db Fix, Improve
- fix range exception when no data fetched from server
- display empty when no server stored
- server edit page auto select stored/used key item
2021-10-31 21:44:02 +08:00
LollipopKit
1943fde6eb remove useless function 2021-10-31 21:40:36 +08:00
LollipopKit
2eb6e19a86 Init snippet page and store 2021-10-31 15:22:05 +08:00
LollipopKit
9f3f07388e Enable analysis.Update dependencies.Update README 2021-10-31 15:21:23 +08:00
LollipopKit
702dd86a84 Add github action 2021-10-30 12:21:39 +08:00
LollipopKit
434ef77c03 Change Licence 2021-10-29 13:33:40 +08:00
183 changed files with 8404 additions and 917 deletions

1
.gitignore vendored
View File

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

6
.vscode/launch.json vendored
View File

@@ -13,7 +13,11 @@
"name": "toolbox (profile mode)", "name": "toolbox (profile mode)",
"request": "launch", "request": "launch",
"type": "dart", "type": "dart",
"flutterMode": "profile" "flutterMode": "profile",
"args": [
"--cache-sksl",
// "--purge-persistent-cache"
]
} }
] ]
} }

View File

@@ -1,44 +1,67 @@
# Server Monitor & Toolbox # 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 ## ScreenShots
<table> <table>
<tr> <tr>
<td> <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>
<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>
<td> <td>
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3385.PNG"> <img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_edit.jpg">
</td> </td>
</tr> </tr>
</table> </table>
<table> <table>
<tr> <tr>
<td> <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>
<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>
<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> </td>
</tr> </tr>
</table> </table>
## Milestone # Support
- [x] SSH Connect Status|Platform
- [x] Server Info Store --|--|
- [x] Status Chart View Full Support|Android/iOS/macOS
- [x] Base64/Url En/Decode Support, but not tested|Windows/Linux
- [x] Private Key Store
- [x] Server Status Detail Page
- [x] Theme Switch ## Build
- [ ] Execute Snippet Please use `make.dart` to build.
```shell
# build android apk and ios archive
./make.dart build
# Run in release mode
./make.dart run release
```
## License ## License
`Apache License. LollipopKit 2021` `LGPL License. LollipopKit 2021`

View File

@@ -22,6 +22,8 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules: rules:
library_private_types_in_public_api: false
use_build_context_synchronously: false
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` 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" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 30 compileSdkVersion 31
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8

View File

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

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

View File

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

1
apk.sksl.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,47 +1,52 @@
PODS: PODS:
- countly_flutter (20.11.4): - countly_flutter (21.11.0):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- GZ-NMSSH (4.1.5) - flutter_native_splash (0.0.1):
- path_provider (0.0.1):
- Flutter - Flutter
- ssh2 (2.2.3): - path_provider_ios (0.0.1):
- Flutter - Flutter
- GZ-NMSSH (~> 4.1.5) - r_upgrade (0.0.1):
- url_launcher (0.0.1): - Flutter
- share_plus (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`) - countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- path_provider (from `.symlinks/plugins/path_provider/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- ssh2 (from `.symlinks/plugins/ssh2/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`) - r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
SPEC REPOS: - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
trunk:
- GZ-NMSSH
EXTERNAL SOURCES: EXTERNAL SOURCES:
countly_flutter: countly_flutter:
:path: ".symlinks/plugins/countly_flutter/ios" :path: ".symlinks/plugins/countly_flutter/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
path_provider: flutter_native_splash:
:path: ".symlinks/plugins/path_provider/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
ssh2: path_provider_ios:
:path: ".symlinks/plugins/ssh2/ios" :path: ".symlinks/plugins/path_provider_ios/ios"
url_launcher: r_upgrade:
:path: ".symlinks/plugins/url_launcher/ios" :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: SPEC CHECKSUMS:
countly_flutter: 38419412e193a1faa5babeb5d28a63fda260687d countly_flutter: e245f94349d8adf306c22e60c10648c69aae7380
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
GZ-NMSSH: d749f8ae2fd0094b953cd1d5abd8e0cab3c93f8d flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
ssh2: 74165efc99417a075ecafd52caf93edadfb5eb60 r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
url_launcher: b6e016d912f04be9f5bf6e8e82dc599b7ba59649 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
COCOAPODS: 1.11.2 COCOAPODS: 1.11.3

View File

@@ -155,7 +155,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1020; LastUpgradeCheck = 1300;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
@@ -354,7 +354,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -362,7 +362,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.toolbox; MARKETING_VERSION = 1.0.136;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -483,7 +484,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -491,7 +492,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.toolbox; MARKETING_VERSION = 1.0.136;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -506,7 +508,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 136;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -514,7 +516,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.toolbox; MARKETING_VERSION = 1.0.136;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1300"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

View File

@@ -11,21 +11,23 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>toolbox</string> <string>ServerBox</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@@ -39,7 +41,12 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>UIStatusBarHidden</key> <key>CFBundleLocalizations</key>
<false/> <array>
</dict> <string>en</string>
</plist> <string>zh</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

1
ipa.sksl.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,9 @@
import 'package:flutter/material.dart'; 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/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/home.dart'; import 'package:toolbox/view/page/home.dart';
@@ -21,16 +25,24 @@ class MyApp extends StatelessWidget {
}); });
} }
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
setTransparentNavigationBar(context);
return ValueListenableBuilder<int>( return ValueListenableBuilder<int>(
valueListenable: locator<SettingStore>().primaryColor.listenable(), valueListenable: locator<SettingStore>().primaryColor.listenable(),
builder: (_, value, __) { builder: (_, value, __) {
final primaryColor = Color(value); final primaryColor = Color(value);
final textStyle = TextStyle(color: primaryColor); final textStyle = TextStyle(color: primaryColor);
return MaterialApp( return MaterialApp(
title: 'ToolBox', localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
title: BuildData.name,
debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
primaryColor: primaryColor, primaryColor: primaryColor,
appBarTheme: AppBarTheme(backgroundColor: primaryColor), appBarTheme: AppBarTheme(backgroundColor: primaryColor),

View File

@@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:countly_flutter/countly_config.dart';
import 'package:countly_flutter/countly_flutter.dart'; import 'package:countly_flutter/countly_flutter.dart';
import 'package:logging/logging.dart';
class Analysis { class Analysis {
static const _url = 'https://countly.xuty.cc'; static const _url = 'https://countly.xuty.cc';
@@ -9,25 +12,29 @@ class Analysis {
static bool _enabled = false; static bool _enabled = false;
static Future<void> init(bool debug) async { static Future<void> init(bool debug) async {
if (_url.isEmpty || _key.isEmpty) { if (Platform.isAndroid || Platform.isIOS) {
return; _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}');
} }
_enabled = true;
await Countly.setLoggingEnabled(debug);
await Countly.init(_url, _key);
await Countly.start();
await Countly.enableCrashReporting();
await Countly.giveAllConsent();
} }
static void recordView(String view) { static void recordView(String view) {
if (!_enabled) return; if (_enabled) {
Countly.recordView(view); Countly.recordView(view);
}
} }
static void recordException(Object exception, [bool fatal = false]) { static void recordException(Object exception, [bool fatal = false]) {
if (!_enabled) return; if (_enabled) {
Countly.logException(exception.toString(), !fatal, null); Countly.logException(exception.toString(), !fatal, null);
}
} }
} }

View File

@@ -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 { extension StringX on String {
int get i => int.parse(this); 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,21 +1,24 @@
// ignore_for_file: avoid_print
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:r_upgrade/r_upgrade.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart'; import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/service/app.dart'; import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
final logger = Logger('UPDATE');
Future<bool> isFileAvailable(String url) async { Future<bool> isFileAvailable(String url) async {
try { try {
final resp = await Dio().head(url); final resp = await Dio().head(url);
return resp.statusCode == 200; return resp.statusCode == 200;
} catch (e) { } catch (e) {
print('update file not available: $e'); logger.warning('update file not available: $e');
return false; return false;
} }
} }
@@ -25,22 +28,47 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
locator<AppProvider>().setNewestBuild(update.newest); locator<AppProvider>().setNewestBuild(update.newest);
if (!force && update.newest <= BuildData.build) { final newest = () {
print('Update ignored due to current: ${BuildData.build}, ' if (Platform.isAndroid) {
'update: ${update.newest}'); 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; return;
} }
print('Update available: ${update.newest}'); logger.info('Update available: $newest');
if (Platform.isAndroid && !await isFileAvailable(update.android)) { if (Platform.isAndroid && !await isFileAvailable(update.android)) {
return; return;
} }
final s = S.of(context);
showSnackBarWithAction( showSnackBarWithAction(
context, context,
update.min > BuildData.build update.min > BuildData.build
? '您的版本过旧,请及时更新' ? 'Your version is too old. \nPlease update to v1.0.$newest.'
: '${BuildData.name}有更新啦Ver${update.newest}\n${update.changelog}', : 'Update: v1.0.$newest available. \n${update.changelog}',
'更新', 'Update', () async {
() => openUrl(Platform.isAndroid ? update.android : update.ios)); if (Platform.isAndroid) {
await RUpgrade.upgrade(update.android,
fileName: update.android.split('/').last, isAutoRequestInstall: true);
} else if (Platform.isIOS) {
await RUpgrade.upgradeFromAppStore('1586449703');
} 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:toolbox/core/persistant_store.dart'; import 'package:toolbox/core/persistant_store.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/view/widget/card_dialog.dart'; import 'package:toolbox/view/widget/card_dialog.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:toolbox/core/extension/stringx.dart';
void unawaited(Future<void> future) {} void unawaited(Future<void> future) {}
@@ -15,22 +18,23 @@ bool isDarkMode(BuildContext context) =>
void showSnackBar(BuildContext context, Widget child) => void showSnackBar(BuildContext context, Widget child) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child)); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child));
void showSnackBarWithAction( void showSnackBarWithAction(BuildContext context, String content, String action,
BuildContext context, String content, String action, Function onTap) { GestureTapCallback onTap) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(content), content: Text(content),
action: SnackBarAction( action: SnackBarAction(
label: action, label: action,
onPressed: () => onTap, onPressed: onTap,
), ),
)); ));
} }
Future<bool> openUrl(String url) async { Future<bool> openUrl(String url) async {
if (!await canLaunch(url)) { final uri = url.uri;
if (!await canLaunchUrl(uri)) {
return false; return false;
} }
final ok = await launch(url, forceSafariVC: false); final ok = await launchUrl(uri);
if (ok == true) { if (ok == true) {
return true; return true;
} }
@@ -71,12 +75,38 @@ Widget buildSwitch(BuildContext context, StoreProperty<bool> prop,
void setTransparentNavigationBar(BuildContext context) { void setTransparentNavigationBar(BuildContext context) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent, systemNavigationBarContrastEnforced: true));
systemNavigationBarContrastEnforced: true,
systemNavigationBarIconBrightness:
isDarkMode(context) ? Brightness.light : Brightness.dark,
));
} }
} }
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

@@ -13,8 +13,12 @@ class AppUpdate {
*/ */
late int newest; late int newest;
late int iosbuild;
late int androidbuild;
late int macbuild;
late String android; late String android;
late String ios; late String ios;
late String mac;
late int min; late int min;
late String changelog; late String changelog;
@@ -27,6 +31,9 @@ class AppUpdate {
}); });
AppUpdate.fromJson(Map<String, dynamic> json) { AppUpdate.fromJson(Map<String, dynamic> json) {
newest = json["newest"]?.toInt(); newest = json["newest"]?.toInt();
macbuild = json["macbuild"]?.toInt();
iosbuild = json["iosbuild"]?.toInt();
androidbuild = json["androidbuild"]?.toInt();
android = json["android"].toString(); android = json["android"].toString();
ios = json["ios"].toString(); ios = json["ios"].toString();
min = json["min"].toInt(); min = json["min"].toInt();
@@ -35,6 +42,9 @@ class AppUpdate {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data["newest"] = newest; data["newest"] = newest;
data["macbuild"] = macbuild;
data["iosbuild"] = iosbuild;
data["androidbuild"] = androidbuild;
data["android"] = android; data["android"] = android;
data["ios"] = ios; data["ios"] = ios;
data["min"] = min; data["min"] = min;

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; return used.isNaN ? 0 : 100 - used * 100;
} }
Cpu2Status update(List<CpuStatus> newStatus, String newTemp) { void update(List<CpuStatus> newStatus, String newTemp) {
return Cpu2Status(now, newStatus, newTemp); pre = now;
now = newStatus;
temp = newTemp;
} }
int get coresCount => now.length; int get coresCount => now.length;

View File

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

View File

@@ -25,23 +25,4 @@ class DiskInfo {
this.size, this.size,
this.avail, this.avail,
); );
DiskInfo.fromJson(Map<String, dynamic> json) {
mountPath = json["mountPath"].toString();
mountLocation = json["mountLocation"].toString();
usedPercent = int.parse(json["usedPercent"]);
used = json["used"].toString();
size = json["size"].toString();
avail = json["avail"].toString();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["mountPath"] = mountPath;
data["mountLocation"] = mountLocation;
data["usedPercent"] = usedPercent;
data["used"] = used;
data["size"] = size;
data["avail"] = avail;
return data;
}
} }

View File

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

View File

@@ -0,0 +1,60 @@
import 'package:toolbox/core/extension/numx.dart';
class NetSpeedPart {
String device;
int bytesIn;
int bytesOut;
int time;
NetSpeedPart(this.device, this.bytesIn, this.bytesOut, this.time);
}
class NetSpeed {
List<NetSpeedPart> old;
List<NetSpeedPart> now;
NetSpeed(this.old, this.now);
List<String> get devices {
final devices = <String>[];
for (var item in now) {
devices.add(item.device);
}
return devices;
}
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';
final idx = deviceIdx(device);
final speedInBytesPerSecond =
(now[idx].bytesIn - old[idx].bytesIn) / timeDiff;
return buildStandardOutput(speedInBytesPerSecond);
}
String speedOut({String? device}) {
if (old[0].device == '' || now[0].device == '') return '0kb/s';
final idx = deviceIdx(device);
final speedInBytesPerSecond =
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
return buildStandardOutput(speedInBytesPerSecond);
}
int deviceIdx(String? device) {
if (device != null) {
for (var item in now) {
if (item.device == device) {
return now.indexOf(item);
}
}
}
return 0;
}
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

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

View File

@@ -1,4 +1,4 @@
import 'package:ssh2/ssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/server_status.dart';
@@ -6,7 +6,7 @@ import 'package:toolbox/data/model/server/server_status.dart';
class ServerInfo { class ServerInfo {
ServerPrivateInfo info; ServerPrivateInfo info;
ServerStatus status; ServerStatus status;
SSHClient client; SSHClient? client;
ServerConnectionState connectionState; ServerConnectionState connectionState;
ServerInfo(this.info, this.status, this.client, this.connectionState); ServerInfo(this.info, this.status, this.client, this.connectionState);

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ class TcpStatus {
passive = json["passive"].toInt(); passive = json["passive"].toInt();
fail = json["fail"].toInt(); fail = json["fail"].toInt();
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data["maxConn"] = maxConn; data["maxConn"] = maxConn;

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( _addWidget(SingleChildScrollView(
child: widget,
scrollDirection: Axis.horizontal, 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,27 +1,65 @@
import 'dart:async'; import 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:ssh2/ssh2.dart';
import 'package:toolbox/core/extension/stringx.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/core/provider_base.dart';
import 'package:toolbox/data/model/server/cpu_2_status.dart'; import 'package:toolbox/data/model/server/cpu_2_status.dart';
import 'package:toolbox/data/model/server/cpu_status.dart'; import 'package:toolbox/data/model/server/cpu_status.dart';
import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/net_speed.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/disk_info.dart'; import 'package:toolbox/data/model/server/disk_info.dart';
import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/model/server/tcp_status.dart'; import 'package:toolbox/data/model/server/tcp_status.dart';
import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
/// Must put this func out of any Class.
/// Because of this function is called by [compute] in [ServerProvider.genClient].
/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure
List<SSHKeyPair> loadIndentity(String key) {
return SSHKeyPair.fromPem(key);
}
const seperator = 'A====A';
const shellCmd = "export LANG=en_US.utf-8 \necho '$seperator' \n"
"cat /proc/net/dev && date +%s \necho $seperator \n "
"cat /etc/os-release | grep PRETTY_NAME \necho $seperator \n"
"cat /proc/stat | grep cpu \necho $seperator \n"
"uptime \necho $seperator \n"
"cat /proc/net/snmp \necho $seperator \n"
"df -h \necho $seperator \n"
"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 { class ServerProvider extends BusyProvider {
List<ServerInfo> _servers = []; List<ServerInfo> _servers = [];
List<ServerInfo> get servers => _servers; List<ServerInfo> get servers => _servers;
Timer? _timer;
final logger = Logger('ServerProvider'); final logger = Logger('ServerProvider');
Memory get emptyMemory =>
Memory(total: 1, used: 0, free: 1, shared: 0, cache: 0, avail: 1);
NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0);
NetSpeed get emptyNetSpeed =>
NetSpeed([emptyNetSpeedPart], [emptyNetSpeedPart]);
CpuStatus get emptyCpuStatus => CpuStatus('cpu', 0, 0, 0, 0, 0, 0, 0); CpuStatus get emptyCpuStatus => CpuStatus('cpu', 0, 0, 0, 0, 0, 0, 0);
Cpu2Status get emptyCpu2Status => Cpu2Status get emptyCpu2Status =>
@@ -29,11 +67,12 @@ class ServerProvider extends BusyProvider {
ServerStatus get emptyStatus => ServerStatus( ServerStatus get emptyStatus => ServerStatus(
emptyCpu2Status, emptyCpu2Status,
[100, 0], emptyMemory,
'', 'Loading...',
'', '',
[DiskInfo('/', '/', 0, '0', '0', '0')], [DiskInfo('/', '/', 0, '0', '0', '0')],
TcpStatus(0, 0, 0, 0)); TcpStatus(0, 0, 0, 0),
emptyNetSpeed);
Future<void> loadLocalData() async { Future<void> loadLocalData() async {
setBusyState(true); setBusyState(true);
@@ -45,138 +84,192 @@ class ServerProvider extends BusyProvider {
ServerInfo genInfo(ServerPrivateInfo spi) { ServerInfo genInfo(ServerPrivateInfo spi) {
return ServerInfo( return ServerInfo(
spi, emptyStatus, genClient(spi), ServerConnectionState.disconnected); spi, emptyStatus, null, ServerConnectionState.disconnected);
} }
SSHClient genClient(ServerPrivateInfo spi) { Future<SSHClient> genClient(ServerPrivateInfo spi) async {
return SSHClient( final socket = await SSHSocket.connect(spi.ip, spi.port);
host: spi.ip!, if (spi.pubKeyId == null) {
port: spi.port!, return SSHClient(socket,
username: spi.user!, username: spi.user,
passwordOrKey: spi.authorization); onPasswordRequest: () => spi.authorization as String);
}
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
return SSHClient(socket,
username: spi.user,
identities: await compute(loadIndentity, key.privateKey));
} }
Future<void> refreshData({int? idx}) async { Future<void> refreshData({ServerPrivateInfo? spi}) async {
if (idx != null) { if (spi != null) {
final singleData = await _getData(_servers[idx].info, idx); _getData(spi);
if (singleData != null) {
_servers[idx].status = singleData;
notifyListeners();
}
return; return;
} }
try { await Future.wait(_servers.map((s) async {
await Future.wait(_servers.map((s) async { await _getData(s.info);
final idx = _servers.indexOf(s); }));
final status = await _getData(s.info, idx);
if (status != null) {
_servers[idx].status = status;
notifyListeners();
}
}));
} catch (e) {
rethrow;
}
} }
Future<void> startAutoRefresh() async { Future<void> startAutoRefresh() async {
final duration = final duration =
locator<SettingStore>().serverStatusUpdateInterval.fetch()!; locator<SettingStore>().serverStatusUpdateInterval.fetch()!;
if (duration == 0) return; if (duration == 0) return;
Timer.periodic(Duration(seconds: duration), (_) async { stopAutoRefresh();
_timer = Timer.periodic(Duration(seconds: duration), (_) async {
await refreshData(); await refreshData();
}); });
} }
void addServer(ServerPrivateInfo info) { void stopAutoRefresh() {
_servers.add(genInfo(info)); if (_timer != null) {
locator<ServerStore>().put(info); _timer!.cancel();
_timer = null;
}
}
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(); notifyListeners();
refreshData(idx: _servers.length - 1); refreshData(spi: spi);
} }
void delServer(ServerPrivateInfo info) { void delServer(ServerPrivateInfo info) {
_servers.removeWhere((e) => e.info == info); final idx = _servers.indexWhere((s) => s.info == info);
if (idx == -1) return;
_servers[idx].client?.close();
_servers.removeAt(idx);
notifyListeners();
locator<ServerStore>().delete(info); locator<ServerStore>().delete(info);
notifyListeners();
} }
void updateServer(ServerPrivateInfo old, ServerPrivateInfo newInfo) { Future<void> updateServer(
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
final idx = _servers.indexWhere((e) => e.info == old); final idx = _servers.indexWhere((e) => e.info == old);
_servers[idx].info = newInfo; if (idx < 0) {
_servers[idx].client = genClient(newInfo); throw RangeError.index(idx, _servers);
locator<ServerStore>().update(old, newInfo); }
_servers[idx].info = newSpi;
_servers[idx].client = await genClient(newSpi);
locator<ServerStore>().update(old, newSpi);
notifyListeners(); notifyListeners();
refreshData(idx: idx); refreshData(spi: newSpi);
} }
Future<ServerStatus?> _getData(ServerPrivateInfo info, int idx) async { Future<void> _getData(ServerPrivateInfo spi) async {
final client = _servers[idx].client; final idx = _servers.indexWhere((element) => element.info == spi);
final connected = await client.isConnected();
final state = _servers[idx].connectionState; final state = _servers[idx].connectionState;
if (!connected || state != ServerConnectionState.connected) { if (_servers[idx].client == null ||
state == ServerConnectionState.failed ||
state == ServerConnectionState.disconnected) {
_servers[idx].connectionState = ServerConnectionState.connecting; _servers[idx].connectionState = ServerConnectionState.connecting;
notifyListeners(); notifyListeners();
final time1 = DateTime.now(); final time1 = DateTime.now();
try { try {
await client.connect(); _servers[idx].client = await genClient(spi);
final time2 = DateTime.now(); final time2 = DateTime.now();
logger.info( 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; _servers[idx].connectionState = ServerConnectionState.connected;
notifyListeners(); _servers[idx]
.client!
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath");
} catch (e) { } catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed; _servers[idx].connectionState = ServerConnectionState.failed;
notifyListeners(); _servers[idx].status.failedInfo = '$e ## ';
logger.warning(e); logger.warning(e);
} finally {
notifyListeners();
} }
} }
try {
final cpu = await client.execute("cat /proc/stat | grep cpu") ?? '';
final cpuTemp = await client.execute(
r"paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/'") ??
'';
final mem = await client.execute('free -m') ?? '';
final sysVer =
await client.execute('cat /etc/os-release | grep PRETTY_NAME') ?? '';
final upTime = await client.execute('uptime') ?? '';
final disk = await client.execute('df -h') ?? '';
final tcp = await client.execute('cat /proc/net/snmp') ?? '';
return ServerStatus( // if client is null, return
_getCPU(cpu, _servers[idx].status.cpu2Status, cpuTemp), final si = _servers[idx];
_getMem(mem), if (si.client == null) return;
_getSysVer(sysVer), final raw = await si.client!.run("sh $shellPath").string;
_getUpTime(upTime), final lines = raw.split(seperator).map((e) => e.trim()).toList();
_getDisk(disk), lines.removeAt(0);
_getTcp(tcp));
try {
_getCPU(spi, lines[2], lines[7], lines[8]);
_getMem(spi, lines[6]);
_getSysVer(spi, lines[1]);
_getUpTime(spi, lines[3]);
_getDisk(spi, lines[5]);
_getTcp(spi, lines[4]);
_getNetSpeed(spi, lines[0]);
} catch (e) { } catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed; _servers[idx].connectionState = ServerConnectionState.failed;
notifyListeners(); servers[idx].status.failedInfo = e.toString();
logger.warning(e); logger.warning(e);
return null; rethrow;
} finally {
notifyListeners();
} }
} }
String _getSysVer(String raw) { /// [raw] example:
/// Inter-| Receive | Transmit
/// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
/// 1635752901
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 - 1]);
final results = <NetSpeedPart>[];
for (int idx = 2; idx < deviceCount; idx++) {
final data = split[idx].trim().split(':');
final device = data.first;
final bytes = data.last.trim().split(' ');
bytes.removeWhere((element) => element == '');
final bytesIn = int.parse(bytes.first);
final bytesOut = int.parse(bytes[8]);
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
}
info.status.netSpeed.update(results);
}
void _getSysVer(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final s = raw.split('='); final s = raw.split('=');
if (s.length == 2) { if (s.length == 2) {
return s[1].replaceAll('"', '').replaceFirst('\n', ''); info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
} }
return '';
} }
String _getCPUTemp(String raw) { String _getCPUTemp(String type, String value) {
final split = raw.split('\n'); 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) { for (var item in split) {
if (item.contains('x86_pkg_temp') || item.contains('cpu_thermal')) { if (item.contains(cpuTempReg)) {
return item.split(' ').last; break;
} }
idx++;
} }
return ''; return '${(int.parse(value.split('\n')[idx].trim()) / 1000).toStringAsFixed(1)}°C';
} }
Cpu2Status _getCPU(String raw, Cpu2Status old, String temp) { void _getCPU(
ServerPrivateInfo spi, String raw, String tempType, String tempValue) {
final info = _servers.firstWhere((e) => e.info == spi);
final List<CpuStatus> cpus = []; final List<CpuStatus> cpus = [];
for (var item in raw.split('\n')) { for (var item in raw.split('\n')) {
@@ -193,35 +286,31 @@ class ServerProvider extends BusyProvider {
int.parse(matches[5]), int.parse(matches[5]),
int.parse(matches[6]))); int.parse(matches[6])));
} }
if (cpus.isEmpty) { if (cpus.isNotEmpty) {
return emptyCpu2Status; info.status.cpu2Status.update(cpus, _getCPUTemp(tempType, tempValue));
} }
return old.update(cpus, _getCPUTemp(temp));
} }
String _getUpTime(String raw) { void _getUpTime(ServerPrivateInfo spi, String raw) {
return raw.split('up ')[1].split(', ')[0]; _servers.firstWhere((e) => e.info == spi).status.uptime =
raw.split('up ')[1].split(', ')[0];
} }
TcpStatus _getTcp(String raw) { void _getTcp(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final lines = raw.split('\n'); final lines = raw.split('\n');
int idx = 0; final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
for (var item in lines) { orElse: () => '');
if (item.contains('Tcp:')) { if (idx != '') {
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);
if (idx == 2) {
final vals = item.split(RegExp(r'\s{1,}'));
return TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
}
} }
return TcpStatus(0, 0, 0, 0);
} }
List<DiskInfo> _getDisk(String disk) { void _getDisk(ServerPrivateInfo spi, String raw) {
final info = _servers.firstWhere((e) => e.info == spi);
final list = <DiskInfo>[]; final list = <DiskInfo>[];
final items = disk.split('\n'); final items = raw.split('\n');
for (var item in items) { for (var item in items) {
if (items.indexOf(item) == 0 || item.isEmpty) { if (items.indexOf(item) == 0 || item.isEmpty) {
continue; continue;
@@ -230,18 +319,33 @@ class ServerProvider extends BusyProvider {
list.add(DiskInfo(vals[0], vals[5], list.add(DiskInfo(vals[0], vals[5],
int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3])); int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3]));
} }
return list; info.status.disk = list;
} }
List<int> _getMem(String mem) { void _getMem(ServerPrivateInfo spi, String raw) {
for (var item in mem.split('\n')) { final info = _servers.firstWhere((e) => e.info == spi);
if (item.contains('Mem:')) { for (var item in raw.split('\n')) {
return RegExp(r'[1-9][0-9]*') if (item.contains(memPrefix)) {
.allMatches(item) final split = item.replaceFirst(memPrefix, '').split(' ');
.map((e) => int.parse(item.substring(e.start, e.end))) split.removeWhere((e) => e == '');
.toList(); final memList = split.map((e) => int.parse(e)).toList();
info.status.memory = Memory(
total: memList[0],
used: memList[1],
free: memList[2],
shared: memList[3],
cache: memList[4],
avail: memList[5]);
break;
} }
} }
return []; }
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

@@ -0,0 +1,46 @@
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';
import 'package:toolbox/locator.dart';
class SnippetProvider extends BusyProvider {
List<Snippet> get snippets => _snippets;
late List<Snippet> _snippets;
void loadData() {
_snippets = locator<SnippetStore>().fetch();
}
void add(Snippet snippet) {
if (have(snippet)) return;
_snippets.add(snippet);
locator<SnippetStore>().put(snippet);
notifyListeners();
}
void del(Snippet snippet) {
if (!have(snippet)) return;
_snippets.removeAt(index(snippet));
locator<SnippetStore>().delete(snippet);
notifyListeners();
}
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 // This file is generated by ./make.dart
class BuildData { class BuildData {
static const String name = "ToolBox"; static const String name = "ServerBox";
static const int build = 43; static const int build = 136;
static const String engine = "Flutter 2.5.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 18116933e7 (13 days ago) • 2021-10-15 10:46:35 -0700\nEngine • revision d3ea636dc5\nTools • Dart 2.14.4\n"; static const String engine =
static const String buildAt = "2021-10-28 21:14:38.326376"; "Flutter 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 int modifications = 2; static const String buildAt = "2022-05-19 15:59:10.728748";
static const int modifications = 15;
} }

View File

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

View File

@@ -0,0 +1,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/widgets.dart';
final appIcon = Image.asset('assets/app_icon.png');

View File

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

View File

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

View File

@@ -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,3 +1,6 @@
const backendUrl = 'https://v2.custed.lolli.tech'; const backendUrl = 'https://res.lolli.tech';
const baseUrl = backendUrl + '/res/toolbox'; const baseUrl = '$backendUrl/toolbox';
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq'; const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
const myGithub = 'https://github.com/LollipopKit';
const rainSunMeGithub = 'https://github.com/RainSunMe';
const issueUrl = 'https://github.com/LollipopKit/flutter_server_box/issues';

View File

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

View File

@@ -12,7 +12,7 @@ class ServerStore extends PersistentStore {
List<ServerPrivateInfo> fetch() { List<ServerPrivateInfo> fetch() {
return getServerInfoList( return getServerInfoList(
json.decode(box.get('servers', defaultValue: '[]')!))!; json.decode(box.get('servers', defaultValue: '[]')!));
} }
void delete(ServerPrivateInfo s) { void delete(ServerPrivateInfo s) {
@@ -23,7 +23,11 @@ class ServerStore extends PersistentStore {
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) { void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
final ss = fetch(); 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)); box.put('servers', json.encode(ss));
} }

View File

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

View File

@@ -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,12 +1,17 @@
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:toolbox/data/provider/app.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/debug.dart';
import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/data/service/app.dart'; import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/data/store/private_key.dart'; import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/data/store/snippet.dart';
GetIt locator = GetIt.instance; GetIt locator = GetIt.instance;
@@ -16,9 +21,13 @@ void setupLocatorForServices() {
void setupLocatorForProviders() { void setupLocatorForProviders() {
locator.registerSingleton(AppProvider()); locator.registerSingleton(AppProvider());
locator.registerSingleton(AptProvider());
locator.registerSingleton(DebugProvider()); locator.registerSingleton(DebugProvider());
locator.registerSingleton(DockerProvider());
locator.registerSingleton(ServerProvider()); locator.registerSingleton(ServerProvider());
locator.registerSingleton(SnippetProvider());
locator.registerSingleton(PrivateKeyProvider()); locator.registerSingleton(PrivateKeyProvider());
locator.registerSingleton(SftpDownloadProvider());
} }
Future<void> setupLocatorForStores() async { Future<void> setupLocatorForStores() async {
@@ -33,6 +42,10 @@ Future<void> setupLocatorForStores() async {
final key = PrivateKeyStore(); final key = PrivateKeyStore();
await key.init(boxName: 'key'); await key.init(boxName: 'key');
locator.registerSingleton(key); locator.registerSingleton(key);
final snippet = SnippetStore();
await snippet.init(boxName: 'snippet');
locator.registerSingleton(snippet);
} }
Future<void> setupLocator() async { Future<void> setupLocator() async {

View File

@@ -7,14 +7,19 @@ import 'package:provider/provider.dart';
import 'package:toolbox/app.dart'; import 'package:toolbox/app.dart';
import 'package:toolbox/core/analysis.dart'; import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/data/provider/app.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/debug.dart';
import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/sftp_download.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
Future<void> initApp() async { Future<void> initApp() async {
await Hive.initFlutter(); await Hive.initFlutter();
await setupLocator(); await setupLocator();
locator<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData(); locator<PrivateKeyProvider>().loadData();
///设置Logger ///设置Logger
@@ -60,9 +65,14 @@ Future<void> main() async {
MultiProvider( MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (_) => locator<AppProvider>()), ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
ChangeNotifierProvider(create: (_) => locator<AptProvider>()),
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()), ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
ChangeNotifierProvider(create: (_) => locator<DockerProvider>()),
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()), ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()), ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
ChangeNotifierProvider(
create: (_) => locator<SftpDownloadProvider>()),
], ],
child: const MyApp(), 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

@@ -2,8 +2,11 @@ import 'dart:convert';
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/res/color.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 { class ConvertPage extends StatefulWidget {
const ConvertPage({Key? key}) : super(key: key); const ConvertPage({Key? key}) : super(key: key);
@@ -18,13 +21,8 @@ class _ConvertPageState extends State<ConvertPage>
late TextEditingController _textEditingControllerResult; late TextEditingController _textEditingControllerResult;
late MediaQueryData _media; late MediaQueryData _media;
late ThemeData _theme; late ThemeData _theme;
late S s;
static const List<String> _typeOption = [
'base64 decode',
'base64 encode',
'URL encode',
'URL decode'
];
int _typeOptionIndex = 0; int _typeOptionIndex = 0;
@override @override
@@ -39,6 +37,7 @@ class _ConvertPageState extends State<ConvertPage>
super.didChangeDependencies(); super.didChangeDependencies();
_media = MediaQuery.of(context); _media = MediaQuery.of(context);
_theme = Theme.of(context); _theme = Theme.of(context);
s = S.of(context);
} }
@override @override
@@ -47,39 +46,32 @@ class _ConvertPageState extends State<ConvertPage>
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7), padding: const EdgeInsets.symmetric(horizontal: 7),
child: AnimationLimiter( controller: ScrollController(),
child: Column( child: Column(children: [
children: AnimationConfiguration.toStaggeredList(
duration: const Duration(milliseconds: 377),
childAnimationBuilder: (widget) => SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: widget,
),
),
children: [
const SizedBox(height: 13), const SizedBox(height: 13),
_buildInputTop(), _buildInputTop(),
_buildTypeOption(), _buildTypeOption(),
_buildResult(), _buildResult(),
], ])),
))),
),
onTap: () => FocusScope.of(context).requestFocus(FocusNode()), onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
_textEditingControllerResult.text = doConvert(); try {
_textEditingControllerResult.text = doConvert();
} catch (e) {
showSnackBar(context, Text('Error: \n$e'));
}
}, },
tooltip: 'convert', tooltip: s.convert,
child: const Icon(Icons.send), child: const Icon(Icons.send),
), ),
); );
} }
String doConvert() { String doConvert() {
final text = _textEditingController.text; final text = _textEditingController.text.trim();
switch (_typeOptionIndex) { switch (_typeOptionIndex) {
case 0: case 0:
return utf8.decode(base64.decode(text)); return utf8.decode(base64.decode(text));
@@ -90,20 +82,29 @@ class _ConvertPageState extends State<ConvertPage>
case 3: case 3:
return Uri.decodeFull(text); return Uri.decodeFull(text);
default: default:
return 'Unknown'; return s.unkownConvertMode;
} }
} }
Widget _buildInputTop() { Widget _buildInputTop() {
return SizedBox( return SizedBox(
height: _media.size.height * 0.33, height: _media.size.height * 0.33,
child: _buildInput(_textEditingController), child: buildInput(context, _textEditingController,
onSubmitted: (_) => _textEditingControllerResult.text = doConvert()),
); );
} }
Widget _buildTypeOption() { Widget _buildTypeOption() {
return Card( final decode = s.decode;
child: ExpansionTile( 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), tilePadding: const EdgeInsets.only(left: 7, right: 27),
childrenPadding: EdgeInsets.zero, childrenPadding: EdgeInsets.zero,
title: Row( title: Row(
@@ -111,7 +112,7 @@ class _ConvertPageState extends State<ConvertPage>
TextButton( TextButton(
style: ButtonStyle( style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)), foregroundColor: MaterialStateProperty.all(primaryColor)),
child: const Icon(Icons.change_circle), child: Icon(Icons.change_circle, semanticLabel: s.upsideDown),
onPressed: () { onPressed: () {
final temp = _textEditingController.text; final temp = _textEditingController.text;
_textEditingController.text = _textEditingControllerResult.text; _textEditingController.text = _textEditingControllerResult.text;
@@ -121,7 +122,7 @@ class _ConvertPageState extends State<ConvertPage>
TextButton( TextButton(
style: ButtonStyle( style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)), foregroundColor: MaterialStateProperty.all(primaryColor)),
child: const Icon(Icons.copy), child: Icon(Icons.copy, semanticLabel: s.copy),
onPressed: () => FlutterClipboard.copy( onPressed: () => FlutterClipboard.copy(
_textEditingControllerResult.text == '' _textEditingControllerResult.text == ''
? ' ' ? ' '
@@ -135,23 +136,23 @@ class _ConvertPageState extends State<ConvertPage>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(_typeOption[_typeOptionIndex], Text(typeOption[_typeOptionIndex],
textScaleFactor: 1.0, textScaleFactor: 1.0,
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: primaryColor)), color: primaryColor)),
const Text( Text(
'Current Mode', s.currentMode,
textScaleFactor: 1.0, textScaleFactor: 1.0,
textAlign: TextAlign.right, 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( .map((e) => ListTile(
title: Text( title: Text(
e, e,
@@ -159,7 +160,7 @@ class _ConvertPageState extends State<ConvertPage>
color: color:
_theme.textTheme.bodyText2!.color!.withAlpha(177)), _theme.textTheme.bodyText2!.color!.withAlpha(177)),
), ),
trailing: _buildRadio(_typeOption.indexOf(e)), trailing: _buildRadio(typeOption.indexOf(e)),
)) ))
.toList(), .toList(),
), ),
@@ -169,20 +170,7 @@ class _ConvertPageState extends State<ConvertPage>
Widget _buildResult() { Widget _buildResult() {
return SizedBox( return SizedBox(
height: _media.size.height * 0.33, height: _media.size.height * 0.33,
child: _buildInput(_textEditingControllerResult), child: buildInput(context, _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,
),
); );
} }

View File

@@ -14,7 +14,7 @@ class _DebugPageState extends State<DebugPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: appBar:
AppBar(title: const Text('Terminal'), backgroundColor: Colors.black), AppBar(title: const Text('App log'), backgroundColor: Colors.black),
body: _buildTerminal(context), body: _buildTerminal(context),
backgroundColor: Colors.black, 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,17 +1,31 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_advanced_drawer/flutter_advanced_drawer.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:toolbox/core/analysis.dart';
import 'package:toolbox/core/build_mode.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/update.dart'; import 'package:toolbox/core/update.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/navigation_item.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/build_data.dart';
import 'package:toolbox/data/res/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/locator.dart';
import 'package:toolbox/view/page/convert.dart'; import 'package:toolbox/view/page/convert.dart';
import 'package:toolbox/view/page/debug.dart'; import 'package:toolbox/view/page/debug.dart';
import 'package:toolbox/view/page/private_key/stored.dart'; import 'package:toolbox/view/page/ping.dart';
import 'package:toolbox/view/page/private_key/list.dart';
import 'package:toolbox/view/page/server/tab.dart'; import 'package:toolbox/view/page/server/tab.dart';
import 'package:toolbox/view/page/setting.dart'; import 'package:toolbox/view/page/setting.dart';
import 'package:toolbox/view/page/sftp/downloaded.dart';
import 'package:toolbox/view/page/snippet/list.dart';
import 'package:toolbox/view/widget/url_text.dart'; import 'package:toolbox/view/widget/url_text.dart';
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
@@ -26,73 +40,229 @@ class _MyHomePageState extends State<MyHomePage>
with with
AutomaticKeepAliveClientMixin, AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin, SingleTickerProviderStateMixin,
AfterLayoutMixin { AfterLayoutMixin,
final List<String> _tabs = ['Servers', 'En/Decode']; WidgetsBindingObserver {
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 @override
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: _tabs.length, vsync: this); _serverProvider = locator<ServerProvider>();
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);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused) {
_serverProvider.setDisconnected();
_serverProvider.stopAutoRefresh();
}
if (state == AppLifecycleState.resumed) {
_serverProvider.startAutoRefresh();
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
setTransparentNavigationBar(context);
super.build(context); super.build(context);
return Scaffold( return WillPopScope(
appBar: AppBar( child: _buildMain(context),
title: GestureDetector( onWillPop: () {
onLongPress: () => if (_advancedDrawerController.value.visible) {
AppRoute(const DebugPage(), 'Debug Page').go(context), _advancedDrawerController.hideDrawer();
child: const Text('ToolBox'), return Future.value(false);
), }
bottom: TabBar( return Future.value(true);
indicatorColor: widget.primaryColor, });
tabs: _tabs.map((e) => Tab(text: e)).toList(), }
controller: _tabController,
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() { Widget _buildDrawer() {
return Drawer( return SafeArea(
child: ListView( child: Column(
padding: EdgeInsets.zero, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
UserAccountsDrawerHeader( _buildIcon(),
accountName: const Text('ToolBox'), const Text(BuildData.name),
accountEmail: Text(_buildVersionStr()), Text(_buildVersionStr()),
currentAccountPicture: _buildIcon(), SizedBox(
height: MediaQuery.of(context).size.height * 0.07,
), ),
ListTile( Padding(
leading: const Icon(Icons.settings), padding: const EdgeInsets.only(left: 29),
title: const Text('Setting'), child: Column(
onTap: () => AppRoute(const SettingPage(), 'Setting').go(context), children: [
), ListTile(
ListTile( leading: const Icon(Icons.settings),
leading: const Icon(Icons.vpn_key), title: Text(s.setting),
title: const Text('Private Key'), onTap: () =>
onTap: () => AppRoute(const SettingPage(), 'Setting').go(context),
AppRoute(const StoredPrivateKeysPage(), 'Setting').go(context), ),
), ListTile(
AboutListTile( leading: const Icon(Icons.vpn_key),
icon: const Icon(Icons.text_snippet), title: Text(s.privateKey),
child: const Text('Licences'), onTap: () => AppRoute(
applicationName: BuildData.name, const StoredPrivateKeysPage(), 'private key list')
applicationVersion: _buildVersionStr(), .go(context),
applicationIcon: _buildIcon(), ),
aboutBoxChildren: const [ ListTile(
UrlText( leading: const Icon(Icons.download),
text: '''\nMade with ❤️ by https://github.com/LollipopKit . title: Text(s.download),
\nAll rights reserved.''', replace: 'LollipopKit'), 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),
)
],
),
), ),
], ],
), ),
@@ -100,14 +270,28 @@ class _MyHomePageState extends State<MyHomePage>
} }
Widget _buildIcon() { Widget _buildIcon() {
return ConstrainedBox( return Stack(
constraints: const BoxConstraints(maxHeight: 60, maxWidth: 60), alignment: Alignment.center,
child: Image.asset('assets/app_icon.png'), 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() { 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 @override
@@ -118,5 +302,8 @@ class _MyHomePageState extends State<MyHomePage>
await GetIt.I.allReady(); await GetIt.I.allReady();
await locator<ServerProvider>().loadLocalData(); await locator<ServerProvider>().loadLocalData();
await doUpdate(context); await doUpdate(context);
if (BuildMode.isRelease) {
await Analysis.init(false);
}
} }
} }

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

@@ -1,7 +1,12 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart'; import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/res/font_style.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/input_decoration.dart'; import 'package:toolbox/view/widget/input_decoration.dart';
@@ -21,19 +26,29 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
final pwdController = TextEditingController(); final pwdController = TextEditingController();
late PrivateKeyProvider _provider; late PrivateKeyProvider _provider;
late Widget loading;
late S s;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_provider = locator<PrivateKeyProvider>(); _provider = locator<PrivateKeyProvider>();
loading = const SizedBox();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [ appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.info != null widget.info != null
? IconButton( ? IconButton(
tooltip: s.delete,
onPressed: () { onPressed: () {
_provider.delInfo(widget.info!); _provider.delInfo(widget.info!);
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -47,7 +62,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
TextField( TextField(
controller: nameController, controller: nameController,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: buildDecoration('Name', icon: Icons.info), decoration: buildDecoration(s.name, icon: Icons.info),
), ),
TextField( TextField(
controller: keyController, controller: keyController,
@@ -56,22 +71,51 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
maxLines: 10, maxLines: 10,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
enableSuggestions: false, enableSuggestions: false,
decoration: buildDecoration('Private Key', icon: Icons.vpn_key), decoration: buildDecoration(s.privateKey, icon: Icons.vpn_key),
), ),
TextField( TextField(
controller: pwdController, controller: pwdController,
autocorrect: false, autocorrect: false,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
obscureText: true, 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( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.send), tooltip: s.save,
onPressed: () { onPressed: () async {
final info = PrivateKeyInfo( final name = nameController.text;
nameController.text, keyController.text, pwdController.text); final key = keyController.text;
final pwd = pwdController.text;
if (name.isEmpty || key.isEmpty) {
showSnackBar(context, Text(s.fieldMustNotEmpty));
return;
}
FocusScope.of(context).unfocus();
setState(() {
loading = const SizedBox(
height: 50,
child: Center(
child: CircularProgressIndicator(),
),
);
});
final info = PrivateKeyInfo(name, key, pwd);
bool haveErr = false;
try {
info.privateKey = await compute(decyptPem, [key, pwd]);
} catch (e) {
showSnackBar(context, Text(e.toString()));
haveErr = true;
} finally {
setState(() {
loading = const SizedBox();
});
}
if (haveErr) return;
if (widget.info != null) { if (widget.info != null) {
_provider.updateInfo(widget.info!, info); _provider.updateInfo(widget.info!, info);
} else { } else {
@@ -79,6 +123,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Icon(Icons.save),
), ),
); );
} }
@@ -92,3 +137,11 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
} }
} }
} }
/// [args] : [key, pwd]
String decyptPem(List<String> args) {
/// skip when the key is not encrypted, or will throw exception
if (!SSHKeyPair.isEncryptedPem(args[0])) return args[0];
final sshKey = SSHKeyPair.fromPem(args[0], args[1]);
return sshKey.first.toPem();
}

View File

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

View File

@@ -1,12 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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.dart';
import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/res/linux_icons.dart'; import 'package:toolbox/data/res/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'; import 'package:toolbox/view/widget/round_rect_card.dart';
const style11 = TextStyle(fontSize: 11);
const style13 = TextStyle(fontSize: 13);
class ServerDetailPage extends StatefulWidget { class ServerDetailPage extends StatefulWidget {
const ServerDetailPage(this.id, {Key? key}) : super(key: key); const ServerDetailPage(this.id, {Key? key}) : super(key: key);
@@ -19,35 +27,39 @@ class ServerDetailPage extends StatefulWidget {
class _ServerDetailPageState extends State<ServerDetailPage> class _ServerDetailPageState extends State<ServerDetailPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late MediaQueryData _media; late MediaQueryData _media;
late S s;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_media = MediaQuery.of(context); _media = MediaQuery.of(context);
s = S.of(context);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<ServerProvider>(builder: (_, provider, __) { return Consumer<ServerProvider>(builder: (_, provider, __) {
return _buildMainPage( return _buildMainPage(provider.servers
provider.servers.firstWhere((e) => e.client.id == widget.id)); .firstWhere((e) => '${e.info.ip}:${e.info.port}' == widget.id));
}); });
} }
Widget _buildMainPage(ServerInfo si) { Widget _buildMainPage(ServerInfo si) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(si.info.name ?? 'Server Detail'), title: Text(si.info.name, style: size18),
), ),
body: ListView( body: ListView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(13),
children: [ children: [
_buildLinuxIcon(si.status.sysVer), _buildLinuxIcon(si.status.sysVer),
SizedBox(height: _media.size.height * 0.03), SizedBox(height: _media.size.height * 0.03),
_buildUpTimeAndSys(si.status), _buildUpTimeAndSys(si.status),
_buildCPUView(si.status), _buildCPUView(si.status),
_buildDiskView(si.status), _buildDiskView(si.status),
_buildMemView(si.status) _buildMemView(si.status),
_buildNetView(si.status.netSpeed),
SizedBox(height: _media.padding.bottom),
], ],
), ),
); );
@@ -56,46 +68,50 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildLinuxIcon(String sysVer) { Widget _buildLinuxIcon(String sysVer) {
final iconPath = linuxIcons.search(sysVer); final iconPath = linuxIcons.search(sysVer);
if (iconPath == null) return const SizedBox(); if (iconPath == null) return const SizedBox();
return SizedBox(height: _media.size.height * 0.15, child: Image.asset(iconPath)); return SizedBox(
height: _media.size.height * 0.15, child: Image.asset(iconPath));
} }
Widget _buildCPUView(ServerStatus ss) { Widget _buildCPUView(ServerStatus ss) {
return RoundRectCard( return RoundRectCard(
SizedBox( Padding(
height: 12 * ss.cpu2Status.coresCount + 67, padding: roundRectCardPadding,
child: Column(children: [ child: SizedBox(
SizedBox( height: 12 * ss.cpu2Status.coresCount + 67,
height: _media.size.height * 0.02, child: Column(children: [
), SizedBox(
Row( height: _media.size.height * 0.02,
mainAxisAlignment: MainAxisAlignment.spaceBetween, ),
children: [ Row(
Text( mainAxisAlignment: MainAxisAlignment.spaceBetween,
'${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%', children: [
style: const TextStyle(fontSize: 27), Text(
textScaleFactor: 1.0, '${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%',
), style: const TextStyle(fontSize: 27),
Row( textScaleFactor: 1.0,
children: [ ),
_buildCPUTimePercent(ss.cpu2Status.user, 'user'), Row(
SizedBox( children: [
width: _media.size.width * 0.03, _buildCPUTimePercent(ss.cpu2Status.user, 'user'),
), SizedBox(
_buildCPUTimePercent(ss.cpu2Status.sys, 'sys'), width: _media.size.width * 0.03,
SizedBox( ),
width: _media.size.width * 0.03, _buildCPUTimePercent(ss.cpu2Status.sys, 'sys'),
), SizedBox(
_buildCPUTimePercent(ss.cpu2Status.nice, 'nice'), width: _media.size.width * 0.03,
SizedBox( ),
width: _media.size.width * 0.03, _buildCPUTimePercent(ss.cpu2Status.nice, 'nice'),
), SizedBox(
_buildCPUTimePercent(ss.cpu2Status.idle, 'idle') width: _media.size.width * 0.03,
], ),
) _buildCPUTimePercent(ss.cpu2Status.idle, 'idle')
], ],
), )
_buildCPUProgress(ss) ],
]), ),
_buildCPUProgress(ss)
]),
),
), ),
); );
} }
@@ -107,7 +123,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
percent.toStringAsFixed(1) + '%', '${percent.toStringAsFixed(1)}%',
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
textScaleFactor: 1.0, textScaleFactor: 1.0,
), ),
@@ -144,19 +160,23 @@ class _ServerDetailPageState extends State<ServerDetailPage>
return LinearProgressIndicator( return LinearProgressIndicator(
value: percentWithinOne, value: percentWithinOne,
minHeight: 7, minHeight: 7,
backgroundColor: Colors.grey[100], backgroundColor: progressColor.resolve(context),
color: pColor.withOpacity(0.5 + percentWithinOne / 2), color: pColor.withOpacity(0.5 + percentWithinOne / 2),
); );
} }
Widget _buildUpTimeAndSys(ServerStatus ss) { Widget _buildUpTimeAndSys(ServerStatus ss) {
return RoundRectCard(Padding( return RoundRectCard(Padding(
padding: const EdgeInsets.symmetric(vertical: 13), padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(ss.sysVer), Text(ss.sysVer, style: style11, textScaleFactor: 1.0),
Text(ss.uptime), Text(
ss.uptime,
style: style11,
textScaleFactor: 1.0,
),
], ],
), ),
)); ));
@@ -164,49 +184,59 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget _buildMemView(ServerStatus ss) { Widget _buildMemView(ServerStatus ss) {
final pColor = primaryColor; final pColor = primaryColor;
final used = ss.memList[1] / ss.memList[0]; final used = ss.memory.used / ss.memory.total;
final width = _media.size.width - 17 * 2 - 17 * 2; final width = _media.size.width - 17 * 2 - 17 * 2;
return RoundRectCard(SizedBox( const mb = 1024 * 1024;
height: 47, return RoundRectCard(Padding(
child: Column( padding: roundRectCardPadding,
crossAxisAlignment: CrossAxisAlignment.center, child: SizedBox(
mainAxisAlignment: MainAxisAlignment.center, height: 47,
children: [ child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildMemExplain('Used', pColor), Row(
_buildMemExplain('Cache', pColor.withAlpha(77)), mainAxisAlignment: MainAxisAlignment.spaceBetween,
_buildMemExplain('Avail', Colors.grey.shade100) children: [
], _buildMemExplain((ss.memory.used * mb).convertBytes, pColor),
), _buildMemExplain(
const SizedBox( (ss.memory.cache * mb).convertBytes, pColor.withAlpha(77)),
height: 7, _buildMemExplain(
), ((ss.memory.total - ss.memory.used) * mb).convertBytes,
Row( progressColor.resolve(context))
children: [ ],
SizedBox( ),
width: width * used, const SizedBox(
height: 7,
),
Row(
children: [
SizedBox(
width: width * used,
child: LinearProgressIndicator(
value: 1,
color: pColor,
)),
SizedBox(
width: width * (1 - used),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: 1, // memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
color: pColor, value: ss.memory.total == 1
)), ? 0
SizedBox( : ss.memory.cache / (ss.memory.total - ss.memory.used),
width: width * (1 - used), backgroundColor: progressColor.resolve(context),
child: LinearProgressIndicator( color: pColor.withAlpha(77),
value: ss.memList[4] / ss.memList[0], ),
backgroundColor: Colors.grey[100], )
color: pColor.withAlpha(77), ],
), )
) ],
], ),
)
],
), ),
)); ));
} }
Widget _buildMemExplain(String type, Color color) { Widget _buildMemExplain(String value, Color color) {
return Row( return Row(
children: [ children: [
Container( Container(
@@ -215,7 +245,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
width: 11, width: 11,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(type, style: const TextStyle(fontSize: 10), textScaleFactor: 1.0) Text(
value,
style: style11,
textScaleFactor: 1.0,
textAlign: TextAlign.center,
)
], ],
); );
} }
@@ -230,7 +265,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
return RoundRectCard(SizedBox( return RoundRectCard(SizedBox(
height: 27 * clone.length + 25, height: 27 * clone.length + 25,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 13), padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: clone.length, itemCount: clone.length,
itemBuilder: (_, idx) { itemBuilder: (_, idx) {
@@ -244,12 +279,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
children: [ children: [
Text( Text(
'${disk.usedPercent}% of ${disk.size}', '${disk.usedPercent}% of ${disk.size}',
style: const TextStyle(fontSize: 11), style: style11,
textScaleFactor: 1.0, textScaleFactor: 1.0,
), ),
Text(disk.mountPath, Text(disk.mountPath, style: style11, textScaleFactor: 1.0)
style: const TextStyle(fontSize: 11),
textScaleFactor: 1.0)
], ],
), ),
_buildProgress(disk.usedPercent.toDouble()) _buildProgress(disk.usedPercent.toDouble())
@@ -260,5 +293,82 @@ class _ServerDetailPageState extends State<ServerDetailPage>
)); ));
} }
static const ignorePath = ['/run', '/sys', '/dev/shm', '/snap', '/var/lib/docker']; Widget _buildNetView(NetSpeed ns) {
final children = <Widget>[
_buildNetSpeedTop(),
const Divider(
height: 7,
)
];
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, horizontal: 17),
child: Column(
children: children,
),
));
}
Widget _buildNetSpeedTop() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.device_hub,
size: 17,
),
Icon(Icons.arrow_upward, size: 17),
Icon(Icons.arrow_downward, size: 17)
],
),
);
}
Widget _buildNetSpeedItem(NetSpeed ns, String device) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: _media.size.width / 4,
child: Text(device, style: style11, textScaleFactor: 1.0)),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedIn(device: device),
style: style11,
textAlign: TextAlign.center,
textScaleFactor: 1.0),
),
SizedBox(
width: _media.size.width / 4,
child: Text(ns.speedOut(device: device),
style: style11,
textAlign: TextAlign.right,
textScaleFactor: 1.0))
],
),
);
}
static const ignorePath = [
'/run',
'/sys',
'/dev/shm',
'/snap',
'/var/lib/docker',
'/dev/tty'
];
} }

View File

@@ -3,10 +3,14 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/private_key_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.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/locator.dart';
import 'package:toolbox/view/page/private_key/edit.dart'; import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/widget/input_decoration.dart'; import 'package:toolbox/view/widget/input_decoration.dart';
@@ -30,10 +34,12 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
late ServerProvider _serverProvider; late ServerProvider _serverProvider;
late S s;
bool usePublicKey = false; bool usePublicKey = false;
int _typeOptionIndex = -1; int _pubKeyIndex = -1;
final List<String> _keyInfo = ['', '']; PrivateKeyInfo? _keyInfo;
@override @override
void initState() { void initState() {
@@ -41,15 +47,35 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_serverProvider = locator<ServerProvider>(); _serverProvider = locator<ServerProvider>();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
s = S.of(context);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Edit'), actions: [ appBar: AppBar(title: Text(s.edit, style: size18), actions: [
widget.spi != null widget.spi != null
? IconButton( ? IconButton(
onPressed: () { onPressed: () {
_serverProvider.delServer(widget.spi!); showRoundDialog(context, s.attention,
Navigator.of(context).pop(); 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)) icon: const Icon(Icons.delete))
: const SizedBox() : const SizedBox()
@@ -63,21 +89,21 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
TextField( TextField(
controller: nameController, controller: nameController,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: decoration: buildDecoration(s.name,
buildDecoration('Name', icon: Icons.info, hint: 'Example'), icon: Icons.info, hint: s.exampleName),
), ),
TextField( TextField(
controller: ipController, controller: ipController,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
autocorrect: false, autocorrect: false,
enableSuggestions: false, enableSuggestions: false,
decoration: buildDecoration('Host', decoration: buildDecoration(s.host,
icon: Icons.storage, hint: 'example.com'), icon: Icons.storage, hint: 'example.com'),
), ),
TextField( TextField(
controller: portController, controller: portController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: buildDecoration('Port', decoration: buildDecoration(s.port,
icon: Icons.format_list_numbered, hint: '22'), icon: Icons.format_list_numbered, hint: '22'),
), ),
TextField( TextField(
@@ -85,13 +111,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
autocorrect: false, autocorrect: false,
enableSuggestions: false, enableSuggestions: false,
decoration: buildDecoration('User', decoration: buildDecoration(s.user,
icon: Icons.account_box, hint: 'root'), icon: Icons.account_box, hint: 'root'),
), ),
const SizedBox(height: 7), const SizedBox(height: 7),
Row( Row(
children: [ children: [
const Text('Public Key Auth'), Text(s.keyAuth),
Switch( Switch(
value: usePublicKey, value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val)), onChanged: (val) => setState(() => usePublicKey = val)),
@@ -102,24 +128,28 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: passwordController, controller: passwordController,
obscureText: true, obscureText: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: buildDecoration('Pwd', decoration: buildDecoration(s.pwd,
icon: Icons.password, hint: 'Password'), icon: Icons.password, hint: s.pwd),
onSubmitted: (_) => {}, onSubmitted: (_) => {},
) )
: const SizedBox(), : const SizedBox(),
usePublicKey usePublicKey
? Consumer<PrivateKeyProvider>(builder: (_, key, __) { ? Consumer<PrivateKeyProvider>(builder: (_, key, __) {
for (var item in key.infos) {
if (item.id == widget.spi?.pubKeyId) {
_pubKeyIndex = key.infos.indexOf(item);
}
}
final tiles = key.infos final tiles = key.infos
.map( .map(
(e) => ListTile( (e) => ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(e.id, textAlign: TextAlign.start), title: Text(e.id, textAlign: TextAlign.start),
trailing: _buildRadio(key.infos.indexOf(e), trailing: _buildRadio(key.infos.indexOf(e), e)),
e.privateKey, e.password)),
) )
.toList(); .toList();
tiles.add(ListTile( tiles.add(ListTile(
title: const Text('Add a Private Key'), title: Text(s.addPrivateKey),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
@@ -133,9 +163,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
iconColor: primaryColor, iconColor: primaryColor,
tilePadding: EdgeInsets.zero, tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero, childrenPadding: EdgeInsets.zero,
title: const Text( title: Text(
'Choose Key', s.choosePrivateKey,
style: TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
children: tiles, children: tiles,
); );
@@ -148,15 +178,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
child: const Icon(Icons.send), child: const Icon(Icons.send),
onPressed: () { onPressed: () {
if (ipController.text == '') { if (ipController.text == '') {
showSnackBar(context, const Text('Please enter host.')); showSnackBar(context, Text(s.plzEnterHost));
return; return;
} }
if (!usePublicKey && passwordController.text == '') { if (!usePublicKey && passwordController.text == '') {
showSnackBar(context, const Text('Please enter password.')); showSnackBar(context, Text(s.plzEnterPwd));
return; return;
} }
if (usePublicKey && _typeOptionIndex == -1) { if (usePublicKey && _pubKeyIndex == -1) {
showSnackBar(context, const Text('Please select a private key.')); showSnackBar(context, Text(s.plzSelectKey));
return; return;
} }
if (usernameController.text == '') { if (usernameController.text == '') {
@@ -165,15 +195,24 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
if (portController.text == '') { if (portController.text == '') {
portController.text = '22'; portController.text = '22';
} }
if (widget.spi != null && widget.spi!.pubKeyId != null) {
_keyInfo ??= locator<PrivateKeyStore>().get(widget.spi!.pubKeyId!);
}
final authorization = usePublicKey final authorization = usePublicKey
? {"privateKey": _keyInfo[0], "passphrase": _keyInfo[1]} ? {
"privateKey": _keyInfo!.privateKey,
"passphrase": _keyInfo!.password
}
: passwordController.text; : passwordController.text;
final spi = ServerPrivateInfo( final spi = ServerPrivateInfo(
name: nameController.text, name: nameController.text,
ip: ipController.text, ip: ipController.text,
port: int.parse(portController.text), port: int.parse(portController.text),
user: usernameController.text, user: usernameController.text,
authorization: authorization); authorization: authorization,
pubKeyId: usePublicKey ? _keyInfo!.id : null);
if (widget.spi == null) { if (widget.spi == null) {
_serverProvider.addServer(spi); _serverProvider.addServer(spi);
@@ -187,15 +226,14 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
); );
} }
Radio _buildRadio(int index, String key, String pwd) { Radio _buildRadio(int index, PrivateKeyInfo pki) {
return Radio<int>( return Radio<int>(
value: index, value: index,
groupValue: _typeOptionIndex, groupValue: _pubKeyIndex,
onChanged: (int? value) { onChanged: (int? value) {
setState(() { setState(() {
_typeOptionIndex = value!; _pubKeyIndex = value!;
_keyInfo[0] = key; _keyInfo = pki;
_keyInfo[1] = pwd;
}); });
}, },
); );
@@ -214,10 +252,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final auth = widget.spi?.authorization as Map; final auth = widget.spi?.authorization as Map;
passwordController.text = auth['passphrase']; passwordController.text = auth['passphrase'];
keyController.text = auth['privateKey']; keyController.text = auth['privateKey'];
setState(() { usePublicKey = true;
usePublicKey = true;
});
} }
setState(() {});
} }
} }
} }

View File

@@ -1,19 +1,29 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:circle_chart/circle_chart.dart'; import 'package:circle_chart/circle_chart.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/app/menu_item.dart';
import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/server_status.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.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/detail.dart';
import 'package:toolbox/view/page/server/edit.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 { class ServerPage extends StatefulWidget {
const ServerPage({Key? key}) : super(key: key); const ServerPage({Key? key}) : super(key: key);
@@ -27,13 +37,16 @@ class _ServerPageState extends State<ServerPage>
late MediaQueryData _media; late MediaQueryData _media;
late ThemeData _theme; late ThemeData _theme;
late Color _primaryColor; late Color _primaryColor;
late RefreshController _refreshController;
late ServerProvider _serverProvider; late ServerProvider _serverProvider;
late S s;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_serverProvider = locator<ServerProvider>(); _serverProvider = locator<ServerProvider>();
_refreshController = RefreshController();
} }
@override @override
@@ -41,38 +54,55 @@ class _ServerPageState extends State<ServerPage>
super.didChangeDependencies(); super.didChangeDependencies();
_media = MediaQuery.of(context); _media = MediaQuery.of(context);
_theme = Theme.of(context); _theme = Theme.of(context);
_primaryColor = Color(locator<SettingStore>().primaryColor.fetch()!); _primaryColor = primaryColor;
s = S.of(context);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final autoUpdate =
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
if (pro.servers.isEmpty) {
return Center(
child: Text(
s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(7),
controller: ScrollController(),
itemBuilder: (ctx, idx) {
if (idx == pro.servers.length) {
return SizedBox(height: _media.padding.bottom);
}
return _buildEachServerCard(pro.servers[idx]);
},
itemCount: pro.servers.length + 1,
separatorBuilder: (_, __) => const SizedBox(
height: 3,
),
);
});
return Scaffold( return Scaffold(
body: SingleChildScrollView( body: autoUpdate
padding: const EdgeInsets.symmetric(horizontal: 7), ? child
child: AnimationLimiter( : SmartRefresher(
child: Consumer<ServerProvider>(builder: (_, pro, __) { controller: _refreshController,
return Column( child: child,
children: AnimationConfiguration.toStaggeredList( onRefresh: () async {
duration: const Duration(milliseconds: 377), await _serverProvider.refreshData();
childAnimationBuilder: (widget) => SlideAnimation( _refreshController.refreshCompleted();
verticalOffset: 50.0, },
child: FadeInAnimation(
child: widget,
),
), ),
children: [
const SizedBox(height: 13),
...pro.servers.map((e) => _buildEachServerCard(e))
],
));
})),
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => onPressed: () =>
AppRoute(const ServerEditPage(), 'Add server info page') AppRoute(const ServerEditPage(), 'Add server info page')
.go(context), .go(context),
tooltip: 'add a server', tooltip: s.addAServer,
heroTag: 'server page fab', heroTag: 'server page fab',
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
@@ -80,119 +110,245 @@ class _ServerPageState extends State<ServerPage>
} }
Widget _buildEachServerCard(ServerInfo si) { Widget _buildEachServerCard(ServerInfo si) {
return GestureDetector( return RoundRectCard(
child: _buildEachCardContent(si), InkWell(
onLongPress: () { onLongPress: () => AppRoute(
AppRoute( ServerEditPage(
ServerEditPage( spi: si.info,
spi: si.info, ),
), 'Edit server info page')
'Edit server info page') .go(context),
.go(context);
});
}
Widget _buildEachCardContent(ServerInfo si) {
return Card(
child: InkWell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
child: _buildRealServerCard( child: _buildRealServerCard(
si.status, si.info.name ?? '', si.connectionState), si.status, si.info.name, si.connectionState, si.info)),
), onTap: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'),
onTap: () => 'server detail page')
AppRoute(ServerDetailPage(si.client.id!), 'server detail page') .go(context),
.go(context),
), ),
); );
} }
Widget _buildRealServerCard( Widget _buildRealServerCard(ServerStatus ss, String serverName,
ServerStatus ss, String serverName, ServerConnectionState cs) { ServerConnectionState cs, ServerPrivateInfo spi) {
final rootDisk = final rootDisk =
ss.disk.firstWhere((element) => element.mountLocation == '/'); ss.disk.firstWhere((element) => element.mountLocation == '/');
final topRightStr =
getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime, ss.failedInfo);
final hasError =
cs == ServerConnectionState.failed && ss.failedInfo != null;
final style = TextStyle(
color: _theme.textTheme.bodyText1!.color!.withAlpha(100), fontSize: 11);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Padding(
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: const EdgeInsets.symmetric(horizontal: 7),
children: [ child: Row(
Text( mainAxisAlignment: MainAxisAlignment.spaceBetween,
serverName, children: [
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), Row(
textScaleFactor: 1.0, children: [
), Text(
Text(getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime), serverName,
textScaleFactor: 1.0, style: const TextStyle(
style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12),
color: _theme.textTheme.bodyText1!.color!.withAlpha(100), textScaleFactor: 1.0,
fontSize: 11)) ),
], const Icon(
Icons.keyboard_arrow_right,
size: 17,
color: Colors.grey,
)
],
),
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( const SizedBox(
height: 17, height: 17,
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
_buildPercentCircle(ss.cpu2Status.usedPercent(), 'CPU'), _buildPercentCircle(ss.cpu2Status.usedPercent()),
_buildPercentCircle(ss.memList[1] / ss.memList[0] * 100, 'Mem'), _buildPercentCircle(ss.memory.used / ss.memory.total * 100),
_buildIOData('Net', 'Conn:\n' + ss.tcp.maxConn.toString(), _buildIOData('Conn:\n${ss.tcp.maxConn}', 'Fail:\n${ss.tcp.fail}'),
'Fail:\n' + ss.tcp.fail.toString()), _buildIOData(
_buildIOData('Disk', 'Total:\n' + rootDisk.size, 'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
'Used:\n' + rootDisk.usedPercent.toString() + '%')
], ],
), ),
const SizedBox(height: 13),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildExplainText('CPU'),
_buildExplainText('Mem'),
_buildExplainText('Net'),
_buildExplainText('Disk'),
],
),
const SizedBox(height: 3),
], ],
); );
} }
String getTopRightStr(ServerConnectionState cs, String temp, String upTime) { 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) { switch (cs) {
case ServerConnectionState.disconnected: case ServerConnectionState.disconnected:
return 'Disconnected'; return s.disconnected;
case ServerConnectionState.connected: 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: case ServerConnectionState.connecting:
return 'Connecting...'; return s.serverTabConnecting;
case ServerConnectionState.failed: case ServerConnectionState.failed:
return 'Failed'; if (failedInfo == null) {
return s.serverTabFailed;
}
if (failedInfo.contains('encypted')) {
return s.serverTabPlzSave;
}
return failedInfo;
default: default:
return 'Unknown State'; return s.serverTabUnkown;
} }
} }
Widget _buildIOData(String title, String up, String down) { Widget _buildIOData(String up, String down) {
final statusTextStyle = TextStyle( final statusTextStyle = TextStyle(
fontSize: 9, color: _theme.textTheme.bodyText1!.color!.withAlpha(177)); fontSize: 9, color: _theme.textTheme.bodyText1!.color!.withAlpha(177));
return SizedBox( return SizedBox(
width: _media.size.width * 0.2, width: _media.size.width * 0.2,
height: _media.size.height * 0.1,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const SizedBox(), const SizedBox(height: 5),
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,
)
],
),
Text( Text(
title, up,
style: const TextStyle(fontSize: 12), style: statusTextStyle,
textAlign: TextAlign.center,
textScaleFactor: 1.0,
),
const SizedBox(height: 3),
Text(
down,
style: statusTextStyle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
textScaleFactor: 1.0, textScaleFactor: 1.0,
) )
@@ -201,41 +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 <= 0) percent = 0.01;
if (percent >= 100) percent = 99.9; if (percent >= 100) percent = 99.9;
return SizedBox( return SizedBox(
width: _media.size.width * 0.2, width: _media.size.width * 0.2,
height: _media.size.height * 0.1, child: Stack(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Stack( Center(
children: [ child: CircleChart(
CircleChart( progressColor: _primaryColor,
progressColor: _primaryColor, progressNumber: percent,
progressNumber: percent, maxNumber: 100,
maxNumber: 100, width: 53,
width: _media.size.width * 0.37, height: 53,
height: _media.size.height * 0.09, ),
),
Positioned.fill(
child: Center(
child: Text(
'${percent.toStringAsFixed(1)}%',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 11),
textScaleFactor: 1.0,
),
),
),
],
), ),
Text( Positioned.fill(
title, child: Center(
style: const TextStyle(fontSize: 12), child: Text(
textAlign: TextAlign.center, '${percent.toStringAsFixed(1)}%',
textScaleFactor: 1.0, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 11),
textScaleFactor: 1.0,
),
),
), ),
], ],
), ),
@@ -250,6 +396,6 @@ class _ServerPageState extends State<ServerPage>
await GetIt.I.allReady(); await GetIt.I.allReady();
await _serverProvider.loadLocalData(); await _serverProvider.loadLocalData();
await _serverProvider.refreshData(); await _serverProvider.refreshData();
await _serverProvider.startAutoRefresh(); _serverProvider.startAutoRefresh();
} }
} }

View File

@@ -1,7 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/update.dart';
import 'package:toolbox/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/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/data/store/setting.dart';
import 'package:toolbox/generated/l10n.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -15,19 +25,30 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> { class _SettingPageState extends State<SettingPage> {
late SettingStore _store; late SettingStore _store;
late int _selectedColorValue; late int _selectedColorValue;
late int _launchPageIdx;
double _intervalValue = 0; double _intervalValue = 0;
late Color priColor; late Color priColor;
static const textStyle = TextStyle(fontSize: 14);
late final ServerProvider _serverProvider;
late MediaQueryData _media;
late ThemeData _theme;
late S s;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
priColor = primaryColor; priColor = primaryColor;
_media = MediaQuery.of(context);
_theme = Theme.of(context);
s = S.of(context);
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_serverProvider = locator<ServerProvider>();
_store = locator<SettingStore>(); _store = locator<SettingStore>();
_launchPageIdx = _store.launchPage.fetch()!;
_intervalValue = _store.serverStatusUpdateInterval.fetch()!.toDouble(); _intervalValue = _store.serverStatusUpdateInterval.fetch()!.toDouble();
} }
@@ -35,70 +56,100 @@ class _SettingPageState extends State<SettingPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Setting'), title: Text(s.setting, style: size18),
), ),
body: ListView( body: ListView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
children: [ children: [
RoundRectCard(_buildAppColorPreview()), _buildAppColorPreview(),
RoundRectCard( _buildUpdateInterval(),
ExpansionTile( _buildCheckUpdate(),
tilePadding: EdgeInsets.zero, _buildLaunchPage()
childrenPadding: EdgeInsets.zero, ].map((e) => RoundRectCard(e)).toList(),
textColor: priColor,
title: const Text(
'Server status update interval',
style: TextStyle(fontSize: 14),
textAlign: TextAlign.start,
),
subtitle: const Text(
'Will take effect the next time app launches.',
style: TextStyle(color: Colors.grey),
),
trailing: Text('${_intervalValue.toInt()} s'),
children: [
Slider(
thumbColor: priColor,
activeColor: priColor.withOpacity(0.7),
min: 0,
max: 10,
value: _intervalValue,
onChanged: (newValue) {
setState(() {
_intervalValue = newValue;
});
},
onChangeEnd: (val) =>
_store.serverStatusUpdateInterval.put(val.toInt()),
label: '${_intervalValue.toInt()} seconds',
divisions: 10,
),
const SizedBox(
height: 3,
),
_intervalValue == 0.0
? const Text('You set to 0, will not update automatically.')
: const SizedBox(),
const SizedBox(
height: 13,
)
],
),
)
],
), ),
); );
} }
Widget _buildCheckUpdate() {
return Consumer<AppProvider>(builder: (_, app, __) {
String display;
if (app.newestBuild != null) {
if (app.newestBuild! > BuildData.build) {
display = s.versionHaveUpdate(app.newestBuild!);
} else {
display = s.versionUpdated(BuildData.build);
}
} else {
display = s.versionUnknownUpdate(BuildData.build);
}
return ListTile(
contentPadding: roundRectCardPadding,
trailing: const Icon(Icons.keyboard_arrow_right),
title: Text(
display,
style: textStyle,
textAlign: TextAlign.start,
),
onTap: () => doUpdate(context, force: true));
});
}
Widget _buildUpdateInterval() {
return ExpansionTile(
tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding,
textColor: priColor,
title: Text(
s.updateServerStatusInterval,
style: textStyle,
textAlign: TextAlign.start,
),
subtitle: Text(
s.willTakEeffectImmediately,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
trailing: Text('${_intervalValue.toInt()} ${s.second}'),
children: [
Slider(
thumbColor: priColor,
activeColor: priColor.withOpacity(0.7),
min: 0,
max: 10,
value: _intervalValue,
onChanged: (newValue) {
setState(() {
_intervalValue = newValue;
});
},
onChangeEnd: (val) {
_store.serverStatusUpdateInterval.put(val.toInt());
_serverProvider.startAutoRefresh();
},
label: '${_intervalValue.toInt()} ${s.second}',
divisions: 10,
),
const SizedBox(
height: 3,
),
_intervalValue == 0.0
? Text(
s.updateIntervalEqual0,
style: const TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
)
: const SizedBox(),
const SizedBox(
height: 13,
)
],
);
}
Widget _buildAppColorPreview() { Widget _buildAppColorPreview() {
return ExpansionTile( return ExpansionTile(
textColor: priColor, textColor: priColor,
tilePadding: EdgeInsets.zero, tilePadding: roundRectCardPadding,
childrenPadding: EdgeInsets.zero, childrenPadding: roundRectCardPadding,
children: [
_buildAppColorPicker(priColor),
_buildColorPickerConfirmBtn()
],
trailing: ClipOval( trailing: ClipOval(
child: Container( child: Container(
color: priColor, color: priColor,
@@ -106,10 +157,14 @@ class _SettingPageState extends State<SettingPage> {
width: 27, width: 27,
), ),
), ),
title: const Text( title: Text(
'App primary color', s.appPrimaryColor,
style: TextStyle(fontSize: 14), style: textStyle,
)); ),
children: [
_buildAppColorPicker(priColor),
_buildColorPickerConfirmBtn()
]);
} }
Widget _buildAppColorPicker(Color selected) { Widget _buildAppColorPicker(Color selected) {
@@ -130,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))
]);
}
}

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