Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f1bd06422 | ||
|
|
06302d7162 | ||
|
|
57ecdddc76 | ||
|
|
04db5a82b9 | ||
|
|
bb1bf9219c | ||
|
|
3ed476275f | ||
|
|
9cbd29d3c1 | ||
|
|
8cbe76d9ff | ||
|
|
1bd673a164 | ||
|
|
4148aac31b | ||
|
|
64196a7ba6 | ||
|
|
cb5aed8e79 | ||
|
|
f60b09abe4 | ||
|
|
d4368f5084 | ||
|
|
330ff9a621 | ||
|
|
0fdc1b784b | ||
|
|
228f228d44 | ||
|
|
b4d42eecf3 | ||
|
|
0533766c1c | ||
|
|
36d7dc7bb2 | ||
|
|
282443a548 | ||
|
|
bfe7140a44 | ||
|
|
7e8e0e2efc | ||
|
|
49f70fe41a | ||
|
|
50762d585c | ||
|
|
6f30976ef7 | ||
|
|
b0c3cd3834 | ||
|
|
d224ad8cf8 | ||
|
|
b824e06736 | ||
|
|
74a933eb6e | ||
|
|
29e3ee0156 | ||
|
|
f9aa3b1728 | ||
|
|
521ff998af | ||
|
|
b0165c3313 | ||
|
|
2a0cc5ca30 | ||
|
|
52b82af52c | ||
|
|
5c59833233 | ||
|
|
9e2d49773f | ||
|
|
fd1b2fc7b0 | ||
|
|
a06ea82f2c | ||
|
|
9b7d33369a | ||
|
|
9663e4174d | ||
|
|
c524db9f59 | ||
|
|
4274e8bed1 | ||
|
|
fa73c4feee | ||
|
|
00cfd0f88d | ||
|
|
f8201f9542 | ||
|
|
f0081e0587 | ||
|
|
e6e08dc407 | ||
|
|
7a5516792c | ||
|
|
241002c3ea | ||
|
|
34e6b99297 | ||
|
|
b800bd91fd | ||
|
|
07fbf78853 | ||
|
|
bb80f093e0 | ||
|
|
fa8ffdc656 | ||
|
|
f07d33a1d6 | ||
|
|
282e61afac | ||
|
|
feb3b10f1f | ||
|
|
1184841c24 | ||
|
|
02a8a38a0a | ||
|
|
d70cbb66d2 | ||
|
|
4636219b6a | ||
|
|
b7b8d33bd9 | ||
|
|
10c874e7d4 | ||
|
|
5bf5a4b67e | ||
|
|
1bd43829bb | ||
|
|
026d203608 | ||
|
|
5d9b19407f | ||
|
|
86be556a22 | ||
|
|
e65d30590a |
4
.fvm/fvm_config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"flutterSdkVersion": "3.3.4",
|
||||||
|
"flavors": {}
|
||||||
|
}
|
||||||
2
.gitignore
vendored
@@ -47,3 +47,5 @@ app.*.map.json
|
|||||||
|
|
||||||
/android/app/fjy.androidstudio.key
|
/android/app/fjy.androidstudio.key
|
||||||
/release
|
/release
|
||||||
|
test.dart
|
||||||
|
.fvm/flutter_sdk
|
||||||
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"dart.flutterSdkPath": ".fvm/flutter_sdk",
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.fvm": true
|
||||||
|
},
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/.fvm": true
|
||||||
|
}
|
||||||
|
}
|
||||||
67
README.md
@@ -1,64 +1,63 @@
|
|||||||
# Server Monitor & Toolbox
|
<!-- Title-->
|
||||||
|
<p align="center">
|
||||||
|
<h1 align="center">Server Box</h1>
|
||||||
|
</p>
|
||||||
|
|
||||||
A new Flutter project which provide a chart view to display server status data.
|
<!-- Badges-->
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://apps.apple.com/app/serverbox-status-tools/id1586449703">
|
||||||
|
<img style="height: 37px" src="screenshots/appstore.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/LollipopKit/flutter_server_box/releases/latest">
|
||||||
|
<img style="height: 37px" src="screenshots/dl-android.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## ScreenShots
|
<p align="center">
|
||||||
|
A Flutter project which provide charts to display server status and tools to manage server. Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## 🔖 Milestone
|
||||||
|
- [x] Status chart view
|
||||||
|
- [x] Snippet ~~market~~, Ping, SFTP, Docker, Apt/Yum and etc.
|
||||||
|
- [x] i18n (English, Chinese)
|
||||||
|
- [x] Desktop support
|
||||||
|
|
||||||
|
## 📱 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.png">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/detail.jpg">
|
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/convert.png">
|
||||||
</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/ping.png">
|
||||||
</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/apt.png">
|
||||||
</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/docker.png">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
# Support
|
## 🖥 Platform
|
||||||
Status|Platform
|
Status|Platform
|
||||||
--|--|
|
--|--|
|
||||||
Full Support|Android/iOS/macOS
|
Full Support|Android/iOS/macOS
|
||||||
Support, but not tested|Windows/Linux
|
Support, but not tested|Windows/Linux
|
||||||
|
|
||||||
## Milestone
|
|
||||||
- [x] SSH connect
|
|
||||||
- [x] Server info store
|
|
||||||
- [x] Status chart view
|
|
||||||
- [x] Base64/Url En/Decode
|
|
||||||
- [x] Private key store
|
|
||||||
- [x] Server status detail page
|
|
||||||
- [x] Theme switch
|
|
||||||
- [x] Execute snippet
|
|
||||||
- [x] Migrate from `ssh2` to `dartssh2`
|
|
||||||
- [x] Desktop support
|
|
||||||
|
|
||||||
## Build
|
## 📝 License
|
||||||
Please use `make.dart` to build.
|
|
||||||
```shell
|
|
||||||
# build android apk
|
|
||||||
./make.dart build android
|
|
||||||
# due to pub package 'ssh2' incompatibility
|
|
||||||
# can't build ios ipa through './make.dart build ios'
|
|
||||||
# more info: [https://github.com/jda258/flutter_ssh2/issues/8]
|
|
||||||
# please run below cmd to run on ios device
|
|
||||||
./make.dart run release
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
`LGPL License. LollipopKit 2021`
|
`LGPL License. LollipopKit 2021`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
BIN
android/app/src/main/res/drawable-night-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
android/app/src/main/res/drawable-night-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
android/app/src/main/res/drawable-night-v21/background.png
Normal file
|
After Width: | Height: | Size: 70 B |
@@ -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>
|
||||||
BIN
android/app/src/main/res/drawable-night-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
android/app/src/main/res/drawable-night-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
android/app/src/main/res/drawable-night-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
android/app/src/main/res/drawable-night/background.png
Normal file
|
After Width: | Height: | Size: 70 B |
@@ -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>
|
||||||
18
android/app/src/main/res/values-night-v31/styles.xml
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
18
android/app/src/main/res/values-v31/styles.xml
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
BIN
assets/linux/wrt.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
@@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>9.0</string>
|
<string>11.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
# platform :ios, '9.0'
|
# platform :ios, '11.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- countly_flutter (20.11.4):
|
- countly_flutter (21.11.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- flutter_native_splash (0.0.1):
|
||||||
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- r_upgrade (0.0.1):
|
- r_upgrade (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- 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`)
|
||||||
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
|
- r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
|
||||||
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
@@ -21,20 +27,26 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/countly_flutter/ios"
|
:path: ".symlinks/plugins/countly_flutter/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_native_splash:
|
||||||
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
r_upgrade:
|
r_upgrade:
|
||||||
:path: ".symlinks/plugins/r_upgrade/ios"
|
:path: ".symlinks/plugins/r_upgrade/ios"
|
||||||
|
share_plus:
|
||||||
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
countly_flutter: 38419412e193a1faa5babeb5d28a63fda260687d
|
countly_flutter: e245f94349d8adf306c22e60c10648c69aae7380
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
|
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
|
||||||
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
|
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||||
|
|
||||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||||
|
|
||||||
COCOAPODS: 1.11.2
|
COCOAPODS: 1.11.3
|
||||||
|
|||||||
@@ -339,7 +339,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -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 = 83;
|
CURRENT_PROJECT_VERSION = 153;
|
||||||
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,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.83;
|
MARKETING_VERSION = 1.0.153;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -467,7 +467,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -484,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 = 83;
|
CURRENT_PROJECT_VERSION = 153;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -492,7 +492,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.83;
|
MARKETING_VERSION = 1.0.153;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -508,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 = 83;
|
CURRENT_PROJECT_VERSION = 153;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.83;
|
MARKETING_VERSION = 1.0.153;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
vendored
Normal file
|
After Width: | Height: | Size: 70 B |
@@ -1,45 +1,52 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>ServerBox</string>
|
<string>ServerBox</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<false/>
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>en</string>
|
||||||
|
<string>zh</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UIStatusBarHidden</key>
|
|
||||||
<false/>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
1
ipa.sksl.json
Normal file
11
lib/app.dart
@@ -1,6 +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/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';
|
||||||
|
|
||||||
@@ -24,12 +27,20 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
@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(
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: S.delegate.supportedLocales,
|
||||||
title: BuildData.name,
|
title: BuildData.name,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
|
|||||||
@@ -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,12 +12,18 @@ class Analysis {
|
|||||||
static bool _enabled = false;
|
static bool _enabled = false;
|
||||||
|
|
||||||
static Future<void> init(bool debug) async {
|
static Future<void> init(bool debug) async {
|
||||||
_enabled = true;
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
await Countly.setLoggingEnabled(debug);
|
_enabled = true;
|
||||||
await Countly.init(_url, _key);
|
final config = CountlyConfig(_url, _key)
|
||||||
await Countly.start();
|
.setLoggingEnabled(debug)
|
||||||
await Countly.enableCrashReporting();
|
.enableCrashReporting();
|
||||||
await Countly.giveAllConsent();
|
await Countly.initWithConfig(config);
|
||||||
|
await Countly.start();
|
||||||
|
await Countly.giveAllConsent();
|
||||||
|
} else {
|
||||||
|
Logger('COUNTLY')
|
||||||
|
.info('Unsupported platform ${Platform.operatingSystem}');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void recordView(String view) {
|
static void recordView(String view) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ enum _BuildMode {
|
|||||||
profile,
|
profile,
|
||||||
}
|
}
|
||||||
|
|
||||||
_BuildMode _buildMode = (() {
|
final _buildMode = (() {
|
||||||
if (const bool.fromEnvironment('dart.vm.product')) {
|
if (const bool.fromEnvironment('dart.vm.product')) {
|
||||||
return _BuildMode.release;
|
return _BuildMode.release;
|
||||||
}
|
}
|
||||||
|
|||||||
11
lib/core/extension/colorx.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/core/extension/numx.dart
Normal 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]}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
10
lib/core/extension/uint8list.dart
Normal 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);
|
||||||
|
}
|
||||||
@@ -9,11 +9,6 @@ class ProviderBase with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProviderState {
|
|
||||||
idle,
|
|
||||||
busy,
|
|
||||||
}
|
|
||||||
|
|
||||||
class BusyProvider extends ProviderBase {
|
class BusyProvider extends ProviderBase {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
bool get isBusy => _isBusy;
|
bool get isBusy => _isBusy;
|
||||||
|
|||||||
@@ -1,22 +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: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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,23 +28,36 @@ 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.${update.newest}.'
|
? 'Your version is too old. \nPlease update to v1.0.$newest.'
|
||||||
: 'Update: v1.0.${update.newest} available. \n${update.changelog}',
|
: 'Update: v1.0.$newest available. \n${update.changelog}',
|
||||||
'Update', () async {
|
s.update, () async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await RUpgrade.upgrade(update.android,
|
await RUpgrade.upgrade(update.android,
|
||||||
fileName: update.android.split('/').last, isAutoRequestInstall: true);
|
fileName: update.android.split('/').last, isAutoRequestInstall: true);
|
||||||
@@ -50,6 +65,11 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
|||||||
await RUpgrade.upgradeFromAppStore('1586449703');
|
await RUpgrade.upgradeFromAppStore('1586449703');
|
||||||
} else if (Platform.isMacOS) {
|
} else if (Platform.isMacOS) {
|
||||||
await RUpgrade.upgradeFromUrl(update.mac);
|
await RUpgrade.upgradeFromUrl(update.mac);
|
||||||
|
} else {
|
||||||
|
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|
||||||
@@ -27,10 +30,11 @@ void showSnackBarWithAction(BuildContext context, String content, String action,
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
52
lib/data/model/app/backup.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:toolbox/data/model/server/private_key_info.dart';
|
||||||
|
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:toolbox/data/model/server/snippet.dart';
|
||||||
|
|
||||||
|
class Backup {
|
||||||
|
// backup format version
|
||||||
|
final int version;
|
||||||
|
final String date;
|
||||||
|
final List<ServerPrivateInfo> spis;
|
||||||
|
final List<Snippet> snippets;
|
||||||
|
final List<PrivateKeyInfo> keys;
|
||||||
|
final int primaryColor;
|
||||||
|
final int serverStatusUpdateInterval;
|
||||||
|
final int launchPage;
|
||||||
|
|
||||||
|
Backup(
|
||||||
|
this.version,
|
||||||
|
this.date,
|
||||||
|
this.spis,
|
||||||
|
this.snippets,
|
||||||
|
this.keys,
|
||||||
|
this.primaryColor,
|
||||||
|
this.serverStatusUpdateInterval,
|
||||||
|
this.launchPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
Backup.fromJson(Map<String, dynamic> json)
|
||||||
|
: version = json['version'] as int,
|
||||||
|
date = json['date'],
|
||||||
|
spis = (json['spis'] as List)
|
||||||
|
.map((e) => ServerPrivateInfo.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
snippets =
|
||||||
|
(json['snippets'] as List).map((e) => Snippet.fromJson(e)).toList(),
|
||||||
|
keys = (json['keys'] as List)
|
||||||
|
.map((e) => PrivateKeyInfo.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
primaryColor = json['primaryColor'],
|
||||||
|
serverStatusUpdateInterval = json['serverStatusUpdateInterval'],
|
||||||
|
launchPage = json['launchPage'];
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'version': version,
|
||||||
|
'date': date,
|
||||||
|
'spis': spis,
|
||||||
|
'snippets': snippets,
|
||||||
|
'keys': keys,
|
||||||
|
'primaryColor': primaryColor,
|
||||||
|
'serverStatusUpdateInterval': serverStatusUpdateInterval,
|
||||||
|
'launchPage': launchPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
44
lib/data/model/app/menu_item.dart
Normal 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);
|
||||||
|
}
|
||||||
8
lib/data/model/app/navigation_item.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NavigationItem {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
NavigationItem(this.icon, this.title);
|
||||||
|
}
|
||||||
38
lib/data/model/app/path_with_prefix.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,9 +31,9 @@ class AppUpdate {
|
|||||||
});
|
});
|
||||||
AppUpdate.fromJson(Map<String, dynamic> json) {
|
AppUpdate.fromJson(Map<String, dynamic> json) {
|
||||||
newest = json["newest"]?.toInt();
|
newest = json["newest"]?.toInt();
|
||||||
newest = json["macbuild"]?.toInt();
|
macbuild = json["macbuild"]?.toInt();
|
||||||
newest = json["iosbuild"]?.toInt();
|
iosbuild = json["iosbuild"]?.toInt();
|
||||||
newest = json["androidbuild"]?.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();
|
||||||
|
|||||||
41
lib/data/model/apt/upgrade_pkg_info.dart
Normal 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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/data/model/distribution.dart
Normal 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'
|
||||||
|
];
|
||||||
37
lib/data/model/docker/ps.dart
Normal 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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -43,5 +45,12 @@ class Cpu2Status {
|
|||||||
return used.isNaN ? 0 : used * 100;
|
return used.isNaN ? 0 : used * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get iowait {
|
||||||
|
if (now.length != pre.length) return 0;
|
||||||
|
final delta = now[0].iowait - pre[0].iowait;
|
||||||
|
final used = delta / totalDelta;
|
||||||
|
return used.isNaN ? 0 : used * 100;
|
||||||
|
}
|
||||||
|
|
||||||
double get idle => 100 - usedPercent();
|
double get idle => 100 - usedPercent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ class Memory {
|
|||||||
int total;
|
int total;
|
||||||
int used;
|
int used;
|
||||||
int free;
|
int free;
|
||||||
int shared;
|
|
||||||
int cache;
|
int cache;
|
||||||
int avail;
|
int avail;
|
||||||
Memory(
|
Memory(
|
||||||
{required this.total,
|
{required this.total,
|
||||||
required this.used,
|
required this.used,
|
||||||
required this.free,
|
required this.free,
|
||||||
required this.shared,
|
|
||||||
required this.cache,
|
required this.cache,
|
||||||
required this.avail});
|
required this.avail});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'dart:math';
|
import 'package:toolbox/core/extension/numx.dart';
|
||||||
|
|
||||||
class NetSpeedPart {
|
class NetSpeedPart {
|
||||||
String device;
|
String device;
|
||||||
@@ -21,51 +21,40 @@ class NetSpeed {
|
|||||||
return devices;
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
NetSpeed update(List<NetSpeedPart> newOne) => NetSpeed(now, newOne);
|
void update(List<NetSpeedPart> newOne) {
|
||||||
|
old = now;
|
||||||
|
now = newOne;
|
||||||
|
}
|
||||||
|
|
||||||
int get timeDiff => now[0].time - old[0].time;
|
int get timeDiff => now[0].time - old[0].time;
|
||||||
|
|
||||||
String speedIn({String? device}) {
|
String speedIn({String? device}) {
|
||||||
if (old[0].device == '' || now[0].device == '') return '0kb/s';
|
if (old[0].device == '' || now[0].device == '') return '0kb/s';
|
||||||
int idx = 0;
|
final idx = deviceIdx(device);
|
||||||
if (device != null) {
|
|
||||||
for (var item in now) {
|
|
||||||
if (item.device == device) {
|
|
||||||
idx = now.indexOf(item);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final speedInBytesPerSecond =
|
final speedInBytesPerSecond =
|
||||||
(now[idx].bytesIn - old[idx].bytesIn) / timeDiff;
|
(now[idx].bytesIn - old[idx].bytesIn) / timeDiff;
|
||||||
int squareTimes = 0;
|
return buildStandardOutput(speedInBytesPerSecond);
|
||||||
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
|
|
||||||
if (squareTimes >= suffixs.length - 1) break;
|
|
||||||
squareTimes++;
|
|
||||||
}
|
|
||||||
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String speedOut({String? device}) {
|
String speedOut({String? device}) {
|
||||||
if (old[0].device == '' || now[0].device == '') return '0kb/s';
|
if (old[0].device == '' || now[0].device == '') return '0kb/s';
|
||||||
int idx = 0;
|
final idx = deviceIdx(device);
|
||||||
|
final speedOutBytesPerSecond =
|
||||||
|
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
|
||||||
|
return buildStandardOutput(speedOutBytesPerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
int deviceIdx(String? device) {
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
for (var item in now) {
|
for (var item in now) {
|
||||||
if (item.device == device) {
|
if (item.device == device) {
|
||||||
idx = now.indexOf(item);
|
return now.indexOf(item);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final speedInBytesPerSecond =
|
return 0;
|
||||||
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
|
|
||||||
int squareTimes = 0;
|
|
||||||
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
|
|
||||||
if (squareTimes >= suffixs.length - 1) break;
|
|
||||||
squareTimes++;
|
|
||||||
}
|
|
||||||
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const suffixs = ['b/s', 'kb/s', 'mb/s', 'gb/s'];
|
String buildStandardOutput(double speed) =>
|
||||||
|
'${speed.convertBytes.toLowerCase()}/s';
|
||||||
|
}
|
||||||
|
|||||||
79
lib/data/model/server/ping_result.dart
Normal 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)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
|
||||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||||
import 'package:toolbox/data/model/server/server_status.dart';
|
import 'package:toolbox/data/model/server/server_status.dart';
|
||||||
|
|
||||||
@@ -11,3 +10,5 @@ class ServerInfo {
|
|||||||
|
|
||||||
ServerInfo(this.info, this.status, this.client, this.connectionState);
|
ServerInfo(this.info, this.status, this.client, this.connectionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ServerConnectionState { disconnected, connecting, connected, failed }
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
enum ServerConnectionState { disconnected, connecting, connected, failed }
|
|
||||||
@@ -17,7 +17,7 @@ class ServerPrivateInfo {
|
|||||||
late String ip;
|
late String ip;
|
||||||
late int port;
|
late int port;
|
||||||
late String user;
|
late String user;
|
||||||
late Object authorization;
|
late String pwd;
|
||||||
String? pubKeyId;
|
String? pubKeyId;
|
||||||
|
|
||||||
ServerPrivateInfo(
|
ServerPrivateInfo(
|
||||||
@@ -25,14 +25,14 @@ class ServerPrivateInfo {
|
|||||||
required this.ip,
|
required this.ip,
|
||||||
required this.port,
|
required this.port,
|
||||||
required this.user,
|
required this.user,
|
||||||
required this.authorization,
|
required this.pwd,
|
||||||
this.pubKeyId});
|
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"];
|
pwd = json["authorization"].toString();
|
||||||
pubKeyId = json["pubKeyId"]?.toString();
|
pubKeyId = json["pubKeyId"]?.toString();
|
||||||
}
|
}
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -41,7 +41,7 @@ class ServerPrivateInfo {
|
|||||||
data["ip"] = ip;
|
data["ip"] = ip;
|
||||||
data["port"] = port;
|
data["port"] = port;
|
||||||
data["user"] = user;
|
data["user"] = user;
|
||||||
data["authorization"] = authorization;
|
data["authorization"] = pwd;
|
||||||
data["pubKeyId"] = pubKeyId;
|
data["pubKeyId"] = pubKeyId;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
29
lib/data/model/sftp/absolute_path.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/data/model/sftp/browser_status.dart
Normal 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();
|
||||||
|
}
|
||||||
58
lib/data/model/sftp/download_status.dart
Normal 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 }
|
||||||
110
lib/data/model/sftp/download_worker.dart
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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.pwd);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
@@ -1,20 +1,9 @@
|
|||||||
import 'package:toolbox/core/provider_base.dart';
|
import 'package:toolbox/core/provider_base.dart';
|
||||||
import 'package:toolbox/data/service/app.dart';
|
|
||||||
|
|
||||||
class AppProvider extends BusyProvider {
|
class AppProvider extends BusyProvider {
|
||||||
Map? _notify;
|
|
||||||
Map? get notify => _notify;
|
|
||||||
int? _newestBuild;
|
int? _newestBuild;
|
||||||
int? get newestBuild => _newestBuild;
|
int? get newestBuild => _newestBuild;
|
||||||
|
|
||||||
Future<void> loadData() async {
|
|
||||||
setBusyState(true);
|
|
||||||
final service = AppService();
|
|
||||||
_notify = await service.getNotify();
|
|
||||||
setBusyState(false);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setNewestBuild(int build) {
|
void setNewestBuild(int build) {
|
||||||
_newestBuild = build;
|
_newestBuild = build;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
176
lib/data/provider/apt.dart
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
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(
|
||||||
|
int triedTimes, 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;
|
||||||
|
bool isRequestingPwd = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
isRequestingPwd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshInstalled() async {
|
||||||
|
if (client == null) {
|
||||||
|
error = 'No client';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _update();
|
||||||
|
getUpgradeableList(result);
|
||||||
|
try {} 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:
|
||||||
|
// avoid other outputs
|
||||||
|
// such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...]
|
||||||
|
final idx =
|
||||||
|
list.indexWhere((element) => element.contains('[upgradable from:'));
|
||||||
|
if (idx == -1) {
|
||||||
|
upgradeable = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list = list.sublist(idx);
|
||||||
|
list.removeWhere((element) => element.isEmpty);
|
||||||
|
}
|
||||||
|
upgradeable = list.map((e) => UpgradePkgInfo(e, dist!)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (isRequestingPwd) return;
|
||||||
|
isRequestingPwd = true;
|
||||||
|
final event = e.string;
|
||||||
|
if (event.contains('[sudo] password for ')) {
|
||||||
|
final user = pwdRequestWithUserReg.firstMatch(event)?.group(1);
|
||||||
|
logger.info('sudo password request for $user');
|
||||||
|
triedTimes++;
|
||||||
|
final pwd =
|
||||||
|
await (onPasswordRequest ?? (_, __) async => '')(triedTimes, user);
|
||||||
|
if (pwd.isEmpty) {
|
||||||
|
logger.info('sudo password request cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stdin.add(Uint8List.fromList(utf8.encode('$pwd\n')));
|
||||||
|
}
|
||||||
|
isRequestingPwd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _wrap(String cmd) =>
|
||||||
|
'export LANG=en_US.utf-8 && ${isSU ? "" : "sudo -S "}$cmd';
|
||||||
|
}
|
||||||
@@ -34,8 +34,8 @@ class DebugProvider extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
_addWidget(SingleChildScrollView(
|
_addWidget(SingleChildScrollView(
|
||||||
child: widget,
|
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: widget,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
96
lib/data/provider/docker.dart
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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');
|
||||||
|
final _versionReg = RegExp(r'(Version:)\s+([0-9]+\.[0-9]+\.[0-9]+)');
|
||||||
|
final _editionReg = RegExp(r'(Client:)\s+(.+-.+)');
|
||||||
|
final _userIdReg = RegExp(r'.+:(\d+:\d+):.+');
|
||||||
|
|
||||||
|
class DockerProvider extends BusyProvider {
|
||||||
|
SSHClient? client;
|
||||||
|
String? userName;
|
||||||
|
List<DockerPsItem>? items;
|
||||||
|
String? version;
|
||||||
|
String? edition;
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
void init(SSHClient client, String userName) {
|
||||||
|
this.client = client;
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
client = null;
|
||||||
|
userName = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
version = _versionReg.firstMatch(verRaw)?.group(2);
|
||||||
|
edition = _editionReg.firstMatch(verRaw)?.group(2);
|
||||||
|
|
||||||
|
final passwd = await client!.run('cat /etc/passwd | grep $userName').string;
|
||||||
|
final userId = _userIdReg.firstMatch(passwd)?.group(1)?.split(':').first;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final cmd = 'docker ps -a'.withLangExport;
|
||||||
|
final raw = await () async {
|
||||||
|
final raw = await client!.run(cmd).string;
|
||||||
|
if (raw.contains('permission denied')) {
|
||||||
|
return await client!
|
||||||
|
.run(
|
||||||
|
'export DOCKER_HOST=unix:///run/user/${userId ?? 1000}/docker.sock && $cmd')
|
||||||
|
.string;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}();
|
||||||
|
final lines = raw.split('\n');
|
||||||
|
lines.removeAt(0);
|
||||||
|
lines.removeWhere((element) => element.isEmpty);
|
||||||
|
items = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _do(String id, String cmd) async {
|
||||||
|
setBusyState();
|
||||||
|
if (client == null) {
|
||||||
|
error = 'no client';
|
||||||
|
setBusyState(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final result = await client!.run(cmd).string;
|
||||||
|
await refresh();
|
||||||
|
setBusyState(false);
|
||||||
|
return result.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stop(String id) async => await _do(id, 'docker stop $id');
|
||||||
|
|
||||||
|
Future<bool> start(String id) async => await _do(id, 'docker start $id');
|
||||||
|
|
||||||
|
Future<bool> delete(String id) async => await _do(id, 'docker rm $id');
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.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/memory.dart';
|
||||||
import 'package:toolbox/data/model/server/net_speed.dart';
|
import 'package:toolbox/data/model/server/net_speed.dart';
|
||||||
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
|
||||||
import 'package:toolbox/data/model/server/disk_info.dart';
|
import 'package:toolbox/data/model/server/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';
|
||||||
@@ -29,6 +28,22 @@ List<SSHKeyPair> loadIndentity(String key) {
|
|||||||
return SSHKeyPair.fromPem(key);
|
return SSHKeyPair.fromPem(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seperator = 'A====A';
|
||||||
|
const shellCmd = "export LANG=en_US.utf-8 \necho '$seperator' \n"
|
||||||
|
"cat /proc/net/dev && date +%s \necho $seperator \n "
|
||||||
|
"cat /etc/os-release | grep PRETTY_NAME \necho $seperator \n"
|
||||||
|
"cat /proc/stat | grep cpu \necho $seperator \n"
|
||||||
|
"uptime \necho $seperator \n"
|
||||||
|
"cat /proc/net/snmp \necho $seperator \n"
|
||||||
|
"df -h \necho $seperator \n"
|
||||||
|
"cat /proc/meminfo \necho $seperator \n"
|
||||||
|
"cat /sys/class/thermal/thermal_zone*/type \necho $seperator \n"
|
||||||
|
"cat /sys/class/thermal/thermal_zone*/temp";
|
||||||
|
const shellPath = '.serverbox.sh';
|
||||||
|
final cpuTempReg = RegExp(r'(x86_pkg_temp|cpu_thermal)');
|
||||||
|
final numReg = RegExp(r'\s{1,}');
|
||||||
|
final memItemReg = RegExp(r'([A-Z].+:)\s+([0-9]+) kB');
|
||||||
|
|
||||||
class ServerProvider extends BusyProvider {
|
class ServerProvider extends BusyProvider {
|
||||||
List<ServerInfo> _servers = [];
|
List<ServerInfo> _servers = [];
|
||||||
List<ServerInfo> get servers => _servers;
|
List<ServerInfo> get servers => _servers;
|
||||||
@@ -38,7 +53,7 @@ class ServerProvider extends BusyProvider {
|
|||||||
final logger = Logger('ServerProvider');
|
final logger = Logger('ServerProvider');
|
||||||
|
|
||||||
Memory get emptyMemory =>
|
Memory get emptyMemory =>
|
||||||
Memory(total: 1, used: 0, free: 1, shared: 0, cache: 0, avail: 1);
|
Memory(total: 1, used: 0, free: 1, cache: 0, avail: 1);
|
||||||
|
|
||||||
NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0);
|
NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0);
|
||||||
|
|
||||||
@@ -76,8 +91,7 @@ class ServerProvider extends BusyProvider {
|
|||||||
final socket = await SSHSocket.connect(spi.ip, spi.port);
|
final socket = await SSHSocket.connect(spi.ip, spi.port);
|
||||||
if (spi.pubKeyId == null) {
|
if (spi.pubKeyId == null) {
|
||||||
return SSHClient(socket,
|
return SSHClient(socket,
|
||||||
username: spi.user,
|
username: spi.user, onPasswordRequest: () => spi.pwd);
|
||||||
onPasswordRequest: () => spi.authorization as String);
|
|
||||||
}
|
}
|
||||||
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
|
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
|
||||||
return SSHClient(socket,
|
return SSHClient(socket,
|
||||||
@@ -85,22 +99,14 @@ class ServerProvider extends BusyProvider {
|
|||||||
identities: await compute(loadIndentity, key.privateKey));
|
identities: await compute(loadIndentity, key.privateKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshData({int? idx}) async {
|
Future<void> refreshData({ServerPrivateInfo? spi}) async {
|
||||||
if (idx != null) {
|
if (spi != null) {
|
||||||
_getData(idx);
|
_getData(spi);
|
||||||
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);
|
}));
|
||||||
if (idx == -1) return;
|
|
||||||
await _getData(idx);
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
if (e is! RangeError) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startAutoRefresh() async {
|
Future<void> startAutoRefresh() async {
|
||||||
@@ -120,15 +126,22 @@ class ServerProvider extends BusyProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addServer(ServerPrivateInfo info) {
|
void setDisconnected() {
|
||||||
_servers.add(genInfo(info));
|
for (var i = 0; i < _servers.length; i++) {
|
||||||
locator<ServerStore>().put(info);
|
_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) {
|
||||||
final idx = _servers.indexWhere((s) => s.info == info);
|
final idx = _servers.indexWhere((s) => s.info == info);
|
||||||
|
if (idx == -1) return;
|
||||||
_servers[idx].client?.close();
|
_servers[idx].client?.close();
|
||||||
_servers.removeAt(idx);
|
_servers.removeAt(idx);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -136,52 +149,76 @@ class ServerProvider extends BusyProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateServer(
|
Future<void> updateServer(
|
||||||
ServerPrivateInfo old, ServerPrivateInfo newInfo) async {
|
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 = await genClient(newInfo);
|
throw RangeError.index(idx, _servers);
|
||||||
locator<ServerStore>().update(old, newInfo);
|
}
|
||||||
|
_servers[idx].info = newSpi;
|
||||||
|
locator<ServerStore>().update(old, newSpi);
|
||||||
|
_servers[idx].client = await genClient(newSpi);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
refreshData(idx: idx);
|
refreshData(spi: newSpi);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getData(int idx) async {
|
Future<void> _getData(ServerPrivateInfo spi) async {
|
||||||
final info = _servers[idx].info;
|
final s = _servers.firstWhere((element) => element.info == spi);
|
||||||
final state = _servers[idx].connectionState;
|
final state = s.connectionState;
|
||||||
if (_servers[idx].client == null ||
|
if (state == ServerConnectionState.failed ||
|
||||||
state == ServerConnectionState.failed ||
|
|
||||||
state == ServerConnectionState.disconnected) {
|
state == ServerConnectionState.disconnected) {
|
||||||
_servers[idx].connectionState = ServerConnectionState.connecting;
|
s.connectionState = ServerConnectionState.connecting;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final time1 = DateTime.now();
|
final time1 = DateTime.now();
|
||||||
try {
|
try {
|
||||||
_servers[idx].client = await genClient(info);
|
s.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;
|
s.connectionState = ServerConnectionState.connected;
|
||||||
notifyListeners();
|
final writeResult = await s.client!
|
||||||
|
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath")
|
||||||
|
.string;
|
||||||
|
if (writeResult.isNotEmpty) {
|
||||||
|
throw Exception(writeResult);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_servers[idx].connectionState = ServerConnectionState.failed;
|
s.connectionState = ServerConnectionState.failed;
|
||||||
_servers[idx].status.failedInfo = e.toString() + ' ## ';
|
s.status.failedInfo = '$e ## ';
|
||||||
notifyListeners();
|
|
||||||
logger.warning(e);
|
logger.warning(e);
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
if (_servers[idx].client == null) return;
|
// if client is null, return
|
||||||
_getCPU(_servers[idx]);
|
if (s.client == null) return;
|
||||||
_getMem(_servers[idx]);
|
final raw = await s.client!.run("sh $shellPath").string;
|
||||||
_getSysVer(_servers[idx]);
|
final segments = raw.split(seperator).map((e) => e.trim()).toList();
|
||||||
_getUpTime(_servers[idx]);
|
if (raw.isEmpty || segments.length == 1) {
|
||||||
_getDisk(_servers[idx]);
|
s.connectionState = ServerConnectionState.failed;
|
||||||
_getTcp(_servers[idx]);
|
if (s.status.failedInfo == null || s.status.failedInfo!.isEmpty) {
|
||||||
_getNetSpeed(_servers[idx]);
|
s.status.failedInfo = 'No data received';
|
||||||
} catch (e) {
|
}
|
||||||
_servers[idx].connectionState = ServerConnectionState.failed;
|
|
||||||
servers[idx].status.failedInfo = e.toString();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
segments.removeAt(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
_getCPU(spi, segments[2], segments[7], segments[8]);
|
||||||
|
_getMem(spi, segments[6]);
|
||||||
|
_getSysVer(spi, segments[1]);
|
||||||
|
_getUpTime(spi, segments[3]);
|
||||||
|
_getDisk(spi, segments[5]);
|
||||||
|
_getTcp(spi, segments[4]);
|
||||||
|
_getNetSpeed(spi, segments[0]);
|
||||||
|
} catch (e) {
|
||||||
|
s.connectionState = ServerConnectionState.failed;
|
||||||
|
s.status.failedInfo = e.toString();
|
||||||
logger.warning(e);
|
logger.warning(e);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,150 +228,162 @@ class ServerProvider extends BusyProvider {
|
|||||||
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
|
/// 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
|
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
|
||||||
/// 1635752901
|
/// 1635752901
|
||||||
Future<void> _getNetSpeed(ServerInfo info) async {
|
Future<void> _getNetSpeed(ServerPrivateInfo spi, String raw) async {
|
||||||
final idx = _servers.indexOf(info);
|
final info = _servers.firstWhere((e) => e.info == spi);
|
||||||
final client = _servers[idx].client!;
|
info.status.netSpeed.update(await compute(_parseNetSpeed, raw));
|
||||||
final raw = utf8.decode(await client.run('cat /proc/net/dev && date +%s'));
|
|
||||||
final split = raw.split('\n');
|
|
||||||
final deviceCount = split.length - 3;
|
|
||||||
if (deviceCount < 1) return;
|
|
||||||
final time = int.parse(split[split.length - 2]);
|
|
||||||
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 = info.status.netSpeed.update(results);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getSysVer(ServerInfo info) async {
|
void _getSysVer(ServerPrivateInfo spi, String raw) {
|
||||||
final idx = _servers.indexOf(info);
|
final info = _servers.firstWhere((e) => e.info == spi);
|
||||||
final client = _servers[idx].client!;
|
|
||||||
final raw =
|
|
||||||
utf8.decode(await client.run('cat /etc/os-release | grep PRETTY_NAME'));
|
|
||||||
final s = raw.split('=');
|
final s = raw.split('=');
|
||||||
if (s.length == 2) {
|
if (s.length == 2) {
|
||||||
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
|
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
|
||||||
} else {
|
|
||||||
info.status.sysVer = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getCPUTemp(String raw) {
|
Future<void> _getCPU(ServerPrivateInfo spi, String raw, String tempType,
|
||||||
final split = raw.split('\n');
|
String tempValue) async {
|
||||||
for (var item in split) {
|
final info = _servers.firstWhere((e) => e.info == spi);
|
||||||
if (item.contains('x86_pkg_temp') || item.contains('cpu_thermal')) {
|
final cpus = await compute(_parseCPU, raw);
|
||||||
return item.split(' ').last;
|
|
||||||
}
|
if (cpus.isNotEmpty) {
|
||||||
|
info.status.cpu2Status
|
||||||
|
.update(cpus, await compute(_getCPUTemp, [tempType, tempValue]));
|
||||||
}
|
}
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getCPU(ServerInfo info) async {
|
void _getUpTime(ServerPrivateInfo spi, String raw) {
|
||||||
final idx = _servers.indexOf(info);
|
_servers.firstWhere((e) => e.info == spi).status.uptime =
|
||||||
final client = _servers[idx].client!;
|
raw.split('up ')[1].split(', ')[0];
|
||||||
final raw = utf8.decode(await client.run("cat /proc/stat | grep cpu"));
|
|
||||||
final temp = utf8.decode(await client.run(
|
|
||||||
r"paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/'"));
|
|
||||||
final List<CpuStatus> cpus = [];
|
|
||||||
|
|
||||||
for (var item in raw.split('\n')) {
|
|
||||||
if (item == '') break;
|
|
||||||
final id = item.split(' ').first;
|
|
||||||
final matches = item.replaceFirst(id, '').trim().split(' ');
|
|
||||||
cpus.add(CpuStatus(
|
|
||||||
id,
|
|
||||||
int.parse(matches[0]),
|
|
||||||
int.parse(matches[1]),
|
|
||||||
int.parse(matches[2]),
|
|
||||||
int.parse(matches[3]),
|
|
||||||
int.parse(matches[4]),
|
|
||||||
int.parse(matches[5]),
|
|
||||||
int.parse(matches[6])));
|
|
||||||
}
|
|
||||||
if (cpus.isEmpty) {
|
|
||||||
info.status.cpu2Status = emptyCpu2Status;
|
|
||||||
} else {
|
|
||||||
info.status.cpu2Status =
|
|
||||||
info.status.cpu2Status.update(cpus, _getCPUTemp(temp));
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getUpTime(ServerInfo info) async {
|
Future<void> _getTcp(ServerPrivateInfo spi, String raw) async {
|
||||||
final idx = _servers.indexOf(info);
|
final info = _servers.firstWhere((e) => e.info == spi);
|
||||||
final client = _servers[idx].client!;
|
final status = await compute(_parseTcp, raw);
|
||||||
final raw = utf8.decode(await client.run('uptime'));
|
if (status != null) {
|
||||||
info.status.uptime = raw.split('up ')[1].split(', ')[0];
|
info.status.tcp = status;
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _getTcp(ServerInfo info) async {
|
|
||||||
final sidx = _servers.indexOf(info);
|
|
||||||
final client = _servers[sidx].client!;
|
|
||||||
final raw = utf8.decode(await client.run('cat /proc/net/snmp'));
|
|
||||||
final lines = raw.split('\n');
|
|
||||||
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
|
|
||||||
orElse: () => '');
|
|
||||||
if (idx != '') {
|
|
||||||
final vals = idx.split(RegExp(r'\s{1,}'));
|
|
||||||
info.status.tcp = TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
|
|
||||||
} else {
|
|
||||||
info.status.tcp = TcpStatus(0, 0, 0, 0);
|
|
||||||
}
|
}
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getDisk(ServerInfo info) async {
|
void _getDisk(ServerPrivateInfo spi, String raw) {
|
||||||
final idx = _servers.indexOf(info);
|
final info = _servers.firstWhere((e) => e.info == spi);
|
||||||
final client = _servers[idx].client!;
|
|
||||||
final disk = utf8.decode(await client.run('df -h'));
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
final vals = item.split(RegExp(r'\s{1,}'));
|
final vals = item.split(numReg);
|
||||||
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]));
|
||||||
}
|
}
|
||||||
info.status.disk = list;
|
info.status.disk = list;
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getMem(ServerInfo info) async {
|
Future<void> _getMem(ServerPrivateInfo spi, String raw) async {
|
||||||
final idx = _servers.indexOf(info);
|
final info = _servers.firstWhere((e) => e.info == spi);
|
||||||
final client = _servers[idx].client!;
|
final mem = await compute(_parseMem, raw);
|
||||||
final mem = utf8.decode(await client.run('free -m'));
|
info.status.memory = mem;
|
||||||
for (var item in mem.split('\n')) {
|
|
||||||
if (item.contains('Mem:')) {
|
|
||||||
final split = item.replaceFirst('Mem:', '').split(' ');
|
|
||||||
split.removeWhere((e) => e == '');
|
|
||||||
final memList = split.map((e) => int.parse(e)).toList();
|
|
||||||
info.status.memory = Memory(
|
|
||||||
total: memList[0],
|
|
||||||
used: memList[1],
|
|
||||||
free: memList[2],
|
|
||||||
shared: memList[3],
|
|
||||||
cache: memList[4],
|
|
||||||
avail: memList[5]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> runSnippet(int idx, Snippet snippet) async {
|
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
|
||||||
final result = await _servers[idx].client!.run(snippet.script);
|
return await _servers
|
||||||
return utf8.decode(result);
|
.firstWhere((element) => element.info == spi)
|
||||||
|
.client!
|
||||||
|
.run(snippet.script)
|
||||||
|
.string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Memory _parseMem(String raw) {
|
||||||
|
final items = raw.split('\n').map((e) => memItemReg.firstMatch(e)).toList();
|
||||||
|
final total = int.parse(
|
||||||
|
items.firstWhere((e) => e?.group(1) == 'MemTotal:')?.group(2) ?? '1');
|
||||||
|
final free = int.parse(
|
||||||
|
items.firstWhere((e) => e?.group(1) == 'MemFree:')?.group(2) ?? '0');
|
||||||
|
final cached = int.parse(
|
||||||
|
items.firstWhere((e) => e?.group(1) == 'Cached:')?.group(2) ?? '0');
|
||||||
|
final available = int.parse(
|
||||||
|
items.firstWhere((e) => e?.group(1) == 'MemAvailable:')?.group(2) ?? '0');
|
||||||
|
return Memory(
|
||||||
|
total: total,
|
||||||
|
used: total - available,
|
||||||
|
free: free,
|
||||||
|
cache: cached,
|
||||||
|
avail: available);
|
||||||
|
}
|
||||||
|
|
||||||
|
TcpStatus? _parseTcp(String raw) {
|
||||||
|
final lines = raw.split('\n');
|
||||||
|
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
|
||||||
|
orElse: () => '');
|
||||||
|
if (idx != '') {
|
||||||
|
final vals = idx.split(numReg);
|
||||||
|
return TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CpuStatus> _parseCPU(String raw) {
|
||||||
|
final List<CpuStatus> cpus = [];
|
||||||
|
|
||||||
|
for (var item in raw.split('\n')) {
|
||||||
|
if (item == '') break;
|
||||||
|
final id = item.split(' ').first;
|
||||||
|
final matches = item.replaceFirst(id, '').trim().split(' ');
|
||||||
|
cpus.add(CpuStatus(
|
||||||
|
id,
|
||||||
|
int.parse(matches[0]),
|
||||||
|
int.parse(matches[1]),
|
||||||
|
int.parse(matches[2]),
|
||||||
|
int.parse(matches[3]),
|
||||||
|
int.parse(matches[4]),
|
||||||
|
int.parse(matches[5]),
|
||||||
|
int.parse(matches[6])));
|
||||||
|
}
|
||||||
|
return cpus;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCPUTemp(List<String> segments) {
|
||||||
|
const noMatch = "/sys/class/thermal/thermal_zone*/type";
|
||||||
|
final type = segments[0];
|
||||||
|
final value = segments[1];
|
||||||
|
// Not support to get CPU temperature
|
||||||
|
if (value.contains(noMatch) ||
|
||||||
|
type.contains(noMatch) ||
|
||||||
|
value.isEmpty ||
|
||||||
|
type.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
final split = type.split('\n');
|
||||||
|
int idx = 0;
|
||||||
|
for (var item in split) {
|
||||||
|
if (item.contains(cpuTempReg)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
final valueSplited = value.split('\n');
|
||||||
|
if (idx >= valueSplited.length) return '';
|
||||||
|
final temp = int.tryParse(valueSplited[idx].trim());
|
||||||
|
if (temp == null) return '';
|
||||||
|
return '${(temp / 1000).toStringAsFixed(1)}°C';
|
||||||
|
}
|
||||||
|
|
||||||
|
List<NetSpeedPart> _parseNetSpeed(String raw) {
|
||||||
|
final split = raw.split('\n');
|
||||||
|
final deviceCount = split.length - 3;
|
||||||
|
if (deviceCount < 1) return [];
|
||||||
|
final time = int.parse(split[split.length - 1]);
|
||||||
|
final results = <NetSpeedPart>[];
|
||||||
|
for (int idx = 2; idx < deviceCount; idx++) {
|
||||||
|
final data = split[idx].trim().split(':');
|
||||||
|
final device = data.first;
|
||||||
|
final bytes = data.last.trim().split(' ');
|
||||||
|
bytes.removeWhere((element) => element == '');
|
||||||
|
final bytesIn = int.parse(bytes.first);
|
||||||
|
final bytesOut = int.parse(bytes[8]);
|
||||||
|
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|||||||
31
lib/data/provider/sftp_download.dart
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:toolbox/core/provider_base.dart';
|
import 'package:toolbox/core/provider_base.dart';
|
||||||
import 'package:toolbox/data/model/server/snippet.dart';
|
import 'package:toolbox/data/model/server/snippet.dart';
|
||||||
import 'package:toolbox/data/store/snippet.dart';
|
import 'package:toolbox/data/store/snippet.dart';
|
||||||
@@ -11,22 +13,34 @@ class SnippetProvider extends BusyProvider {
|
|||||||
_snippets = locator<SnippetStore>().fetch();
|
_snippets = locator<SnippetStore>().fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addInfo(Snippet snippet) {
|
void add(Snippet snippet) {
|
||||||
|
if (have(snippet)) return;
|
||||||
_snippets.add(snippet);
|
_snippets.add(snippet);
|
||||||
locator<SnippetStore>().put(snippet);
|
locator<SnippetStore>().put(snippet);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void delInfo(Snippet snippet) {
|
void del(Snippet snippet) {
|
||||||
_snippets.removeWhere((e) => e.name == snippet.name);
|
if (!have(snippet)) return;
|
||||||
|
_snippets.removeAt(index(snippet));
|
||||||
locator<SnippetStore>().delete(snippet);
|
locator<SnippetStore>().delete(snippet);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateInfo(Snippet old, Snippet newOne) {
|
int index(Snippet snippet) {
|
||||||
final idx = _snippets.indexWhere((e) => e.name == old.name);
|
return _snippets.indexWhere((e) => e.name == snippet.name);
|
||||||
_snippets[idx] = newOne;
|
}
|
||||||
|
|
||||||
|
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);
|
locator<SnippetStore>().update(old, newOne);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get export => json.encode(snippets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
class BuildData {
|
class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 83;
|
static const int build = 154;
|
||||||
static const String engine =
|
static const String engine =
|
||||||
"Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (4 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
|
"Flutter 3.3.4 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision eb6d86ee27 (2 weeks ago) • 2022-10-04 22:31:45 -0700\nEngine • revision c08d7d5efc\nTools • Dart 2.18.2 • DevTools 2.15.0\n";
|
||||||
static const String buildAt = "2022-01-16 15:14:10.759902";
|
static const String buildAt = "2022-10-21 17:29:42.809139";
|
||||||
static const int modifications = 6;
|
static const int modifications = 9;
|
||||||
}
|
}
|
||||||
|
|||||||
4
lib/data/res/font_style.dart
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const TextStyle size18 = TextStyle(fontSize: 18);
|
||||||
|
const TextStyle grey = TextStyle(color: Colors.grey);
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
import 'package:toolbox/data/model/server/linux_icon.dart';
|
import 'package:toolbox/data/model/server/linux_icon.dart';
|
||||||
|
|
||||||
final linuxIcons = LinuxIcons(
|
final linuxIcons = LinuxIcons([
|
||||||
['ubuntu', 'arch', 'centos', 'debian', 'fedora', 'opensuse', 'kali']);
|
'ubuntu',
|
||||||
|
'arch',
|
||||||
|
'centos',
|
||||||
|
'debian',
|
||||||
|
'fedora',
|
||||||
|
'opensuse',
|
||||||
|
'kali',
|
||||||
|
'wrt'
|
||||||
|
]);
|
||||||
|
|||||||
3
lib/data/res/padding.dart
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const roundRectCardPadding = EdgeInsets.symmetric(horizontal: 17);
|
||||||
10
lib/data/res/path.dart
Normal 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
@@ -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'),
|
||||||
|
];
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
const backendUrl = 'https://res.lolli.tech';
|
const backendUrl = 'https://res.lolli.tech';
|
||||||
const baseUrl = backendUrl + '/toolbox';
|
const baseUrl = '$backendUrl/toolbox';
|
||||||
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
|
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
|
||||||
const myGithub = 'https://github.com/LollipopKit';
|
const myGithub = 'https://github.com/LollipopKit';
|
||||||
const rainSunMeGithub = 'https://github.com/RainSunMe';
|
const rainSunMeGithub = 'https://github.com/RainSunMe';
|
||||||
|
const fectureGithub = 'https://github.com/fecture';
|
||||||
|
const issueUrl = 'https://github.com/LollipopKit/flutter_server_box/issues';
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import 'package:toolbox/data/model/app/update.dart';
|
|||||||
import 'package:toolbox/data/res/url.dart';
|
import 'package:toolbox/data/res/url.dart';
|
||||||
|
|
||||||
class AppService {
|
class AppService {
|
||||||
Future<Map> getNotify() async {
|
|
||||||
final resp = await Dio().get('$baseUrl/notify.json');
|
|
||||||
return resp.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<AppUpdate> getUpdate() async {
|
Future<AppUpdate> getUpdate() async {
|
||||||
final resp = await Dio().get('$baseUrl/update.json');
|
final resp = await Dio().get('$baseUrl/update.json');
|
||||||
return AppUpdate.fromJson(resp.data);
|
return AppUpdate.fromJson(resp.data);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
66
lib/generated/intl/messages_all.dart
Normal 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);
|
||||||
|
}
|
||||||
230
lib/generated/intl/messages_en.dart
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||||
|
// This is a library that provides messages for a en locale. All the
|
||||||
|
// messages from the main program should be duplicated here with the same
|
||||||
|
// function name.
|
||||||
|
|
||||||
|
// Ignore issues from commonly used lints in this file.
|
||||||
|
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||||
|
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||||
|
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||||
|
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||||
|
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||||
|
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:intl/message_lookup_by_library.dart';
|
||||||
|
|
||||||
|
final messages = new MessageLookup();
|
||||||
|
|
||||||
|
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||||
|
|
||||||
|
class MessageLookup extends MessageLookupByLibrary {
|
||||||
|
String get localeName => 'en';
|
||||||
|
|
||||||
|
static String m0(fileName) => "Download [${fileName}] to local?";
|
||||||
|
|
||||||
|
static String m1(runningCount, stoppedCount) =>
|
||||||
|
"${runningCount} running, ${stoppedCount} container stopped.";
|
||||||
|
|
||||||
|
static String m2(count) => "${count} container running.";
|
||||||
|
|
||||||
|
static String m3(percent, size) => "${percent}% of ${size}";
|
||||||
|
|
||||||
|
static String m4(count) => "Found ${count} update";
|
||||||
|
|
||||||
|
static String m5(code) => "request failed, status code: ${code}";
|
||||||
|
|
||||||
|
static String m6(url) =>
|
||||||
|
"Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don\'t have the above issues, please submit an issue on ${url}.";
|
||||||
|
|
||||||
|
static String m7(myGithub) => "\nMade with ❤️ by ${myGithub}";
|
||||||
|
|
||||||
|
static String m8(url) => "Please report bugs on ${url}";
|
||||||
|
|
||||||
|
static String m9(date) => "Are you sure to restore from ${date} ?";
|
||||||
|
|
||||||
|
static String m10(time) => "Spent time: ${time}";
|
||||||
|
|
||||||
|
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": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"\nAll rights reserved.\n\nThanks to the following people who participated in the test."),
|
||||||
|
"addAServer": MessageLookupByLibrary.simpleMessage("add a server"),
|
||||||
|
"addPrivateKey":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Add private key"),
|
||||||
|
"alreadyLastDir":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Already in last directory."),
|
||||||
|
"appPrimaryColor":
|
||||||
|
MessageLookupByLibrary.simpleMessage("App primary color"),
|
||||||
|
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
|
||||||
|
"backDir": MessageLookupByLibrary.simpleMessage("Back"),
|
||||||
|
"backup": MessageLookupByLibrary.simpleMessage("Backup"),
|
||||||
|
"backupTip": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting)."),
|
||||||
|
"backupVersionNotMatch": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Backup version is not match."),
|
||||||
|
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||||
|
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
|
||||||
|
"chooseDestination":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Choose destination"),
|
||||||
|
"choosePrivateKey":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Choose private key"),
|
||||||
|
"clear": MessageLookupByLibrary.simpleMessage("Clear"),
|
||||||
|
"clickSee": MessageLookupByLibrary.simpleMessage("Click here"),
|
||||||
|
"close": MessageLookupByLibrary.simpleMessage("Close"),
|
||||||
|
"containerStatus":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Container status"),
|
||||||
|
"convert": MessageLookupByLibrary.simpleMessage("Convert"),
|
||||||
|
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
|
||||||
|
"copyPath": MessageLookupByLibrary.simpleMessage("Copy path"),
|
||||||
|
"createFile": MessageLookupByLibrary.simpleMessage("Create file"),
|
||||||
|
"createFolder": MessageLookupByLibrary.simpleMessage("Create folder"),
|
||||||
|
"currentMode": MessageLookupByLibrary.simpleMessage("Current Mode"),
|
||||||
|
"debug": MessageLookupByLibrary.simpleMessage("Debug"),
|
||||||
|
"decode": MessageLookupByLibrary.simpleMessage("Decode"),
|
||||||
|
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
|
||||||
|
"disconnected": MessageLookupByLibrary.simpleMessage("Disconnected"),
|
||||||
|
"dl2Local": m0,
|
||||||
|
"dockerStatusRunningAndStoppedFmt": m1,
|
||||||
|
"dockerStatusRunningFmt": m2,
|
||||||
|
"download": MessageLookupByLibrary.simpleMessage("Download"),
|
||||||
|
"downloadFinished":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Download finished"),
|
||||||
|
"downloadStatus": m3,
|
||||||
|
"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"),
|
||||||
|
"feedback": MessageLookupByLibrary.simpleMessage("Feedback"),
|
||||||
|
"feedbackOnGithub": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"If you have any questions, please feedback on Github."),
|
||||||
|
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"These fields must not be empty."),
|
||||||
|
"files": MessageLookupByLibrary.simpleMessage("Files"),
|
||||||
|
"foundNUpdate": m4,
|
||||||
|
"go": MessageLookupByLibrary.simpleMessage("Go"),
|
||||||
|
"goSftpDlPage":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"),
|
||||||
|
"host": MessageLookupByLibrary.simpleMessage("Host"),
|
||||||
|
"httpFailedWithCode": m5,
|
||||||
|
"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."),
|
||||||
|
"invalidJson": MessageLookupByLibrary.simpleMessage("Invalid JSON"),
|
||||||
|
"invalidVersionHelp": m6,
|
||||||
|
"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": m7,
|
||||||
|
"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."),
|
||||||
|
"pingNoServer": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"No server to ping.\nPlease add a server in server tab."),
|
||||||
|
"platformNotSupportUpdate": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Current platform does not support in app update.\nPlease build from source and install it."),
|
||||||
|
"plzEnterHost":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Please enter host."),
|
||||||
|
"plzSelectKey":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Please select a key."),
|
||||||
|
"port": MessageLookupByLibrary.simpleMessage("Port"),
|
||||||
|
"privateKey": MessageLookupByLibrary.simpleMessage("Private Key"),
|
||||||
|
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
|
||||||
|
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
|
||||||
|
"reportBugsOnGithubIssue": m8,
|
||||||
|
"restore": MessageLookupByLibrary.simpleMessage("Restore"),
|
||||||
|
"restoreSuccess": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Restore success. Restart app to apply."),
|
||||||
|
"restoreSureWithDate": m9,
|
||||||
|
"result": MessageLookupByLibrary.simpleMessage("Result"),
|
||||||
|
"run": MessageLookupByLibrary.simpleMessage("Run"),
|
||||||
|
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
||||||
|
"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,
|
||||||
|
"sureNoPwd": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Are you sure to use no password?"),
|
||||||
|
"sureToDeleteServer": m12,
|
||||||
|
"ttl": MessageLookupByLibrary.simpleMessage("ttl"),
|
||||||
|
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
|
||||||
|
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
|
||||||
|
"unkownConvertMode":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Unknown convert mode"),
|
||||||
|
"update": MessageLookupByLibrary.simpleMessage("Update"),
|
||||||
|
"updateAll": MessageLookupByLibrary.simpleMessage("Update all"),
|
||||||
|
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"You set to 0, will not update automatically.\nYou can pull to refresh manually."),
|
||||||
|
"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")
|
||||||
|
};
|
||||||
|
}
|
||||||
202
lib/generated/intl/messages_zh.dart
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||||
|
// This is a library that provides messages for a zh locale. All the
|
||||||
|
// messages from the main program should be duplicated here with the same
|
||||||
|
// function name.
|
||||||
|
|
||||||
|
// Ignore issues from commonly used lints in this file.
|
||||||
|
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||||
|
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||||
|
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||||
|
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||||
|
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||||
|
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:intl/message_lookup_by_library.dart';
|
||||||
|
|
||||||
|
final messages = new MessageLookup();
|
||||||
|
|
||||||
|
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||||
|
|
||||||
|
class MessageLookup extends MessageLookupByLibrary {
|
||||||
|
String get localeName => 'zh';
|
||||||
|
|
||||||
|
static String m0(fileName) => "下载 [${fileName}] 到本地?";
|
||||||
|
|
||||||
|
static String m1(runningCount, stoppedCount) =>
|
||||||
|
"${runningCount}个正在运行, ${stoppedCount}个已停止";
|
||||||
|
|
||||||
|
static String m2(count) => "${count}个容器正在运行";
|
||||||
|
|
||||||
|
static String m3(percent, size) => "${size} 的 ${percent}%";
|
||||||
|
|
||||||
|
static String m4(count) => "找到 ${count} 个更新";
|
||||||
|
|
||||||
|
static String m5(code) => "请求失败, 状态码: ${code}";
|
||||||
|
|
||||||
|
static String m6(url) =>
|
||||||
|
"请确保正确安装了docker,或者使用的非自编译版本。如果没有以上问题,请在 ${url} 提交问题。";
|
||||||
|
|
||||||
|
static String m7(myGithub) => "\n用❤️制作 by ${myGithub}";
|
||||||
|
|
||||||
|
static String m8(url) => "请到 ${url} 提交问题";
|
||||||
|
|
||||||
|
static String m9(date) => "确定恢复 ${date} 的备份吗?";
|
||||||
|
|
||||||
|
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":
|
||||||
|
MessageLookupByLibrary.simpleMessage("\n保留所有权利。\n\n感谢以下参与软件测试的各位。"),
|
||||||
|
"addAServer": MessageLookupByLibrary.simpleMessage("添加服务器"),
|
||||||
|
"addPrivateKey": MessageLookupByLibrary.simpleMessage("添加一个私钥"),
|
||||||
|
"alreadyLastDir": MessageLookupByLibrary.simpleMessage("已经是最上层目录了"),
|
||||||
|
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
|
||||||
|
"attention": MessageLookupByLibrary.simpleMessage("注意"),
|
||||||
|
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
|
||||||
|
"backup": MessageLookupByLibrary.simpleMessage("备份"),
|
||||||
|
"backupTip": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"导出的数据仅进行了简单加密,请妥善保管。\n除了设置项,恢复的数据不会覆盖现有数据。"),
|
||||||
|
"backupVersionNotMatch":
|
||||||
|
MessageLookupByLibrary.simpleMessage("备份版本不匹配,无法恢复"),
|
||||||
|
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||||
|
"choose": MessageLookupByLibrary.simpleMessage("选择"),
|
||||||
|
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
|
||||||
|
"choosePrivateKey": MessageLookupByLibrary.simpleMessage("选择私钥"),
|
||||||
|
"clear": MessageLookupByLibrary.simpleMessage("清除"),
|
||||||
|
"clickSee": MessageLookupByLibrary.simpleMessage("点击查看"),
|
||||||
|
"close": MessageLookupByLibrary.simpleMessage("关闭"),
|
||||||
|
"containerStatus": MessageLookupByLibrary.simpleMessage("容器状态"),
|
||||||
|
"convert": MessageLookupByLibrary.simpleMessage("转换"),
|
||||||
|
"copy": MessageLookupByLibrary.simpleMessage("复制到剪切板"),
|
||||||
|
"copyPath": MessageLookupByLibrary.simpleMessage("复制路径"),
|
||||||
|
"createFile": MessageLookupByLibrary.simpleMessage("创建文件"),
|
||||||
|
"createFolder": MessageLookupByLibrary.simpleMessage("创建文件夹"),
|
||||||
|
"currentMode": MessageLookupByLibrary.simpleMessage("当前模式"),
|
||||||
|
"debug": MessageLookupByLibrary.simpleMessage("调试"),
|
||||||
|
"decode": MessageLookupByLibrary.simpleMessage("解码"),
|
||||||
|
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
||||||
|
"disconnected": MessageLookupByLibrary.simpleMessage("连接断开"),
|
||||||
|
"dl2Local": m0,
|
||||||
|
"dockerStatusRunningAndStoppedFmt": m1,
|
||||||
|
"dockerStatusRunningFmt": m2,
|
||||||
|
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
||||||
|
"downloadFinished": MessageLookupByLibrary.simpleMessage("下载完成!"),
|
||||||
|
"downloadStatus": m3,
|
||||||
|
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
||||||
|
"encode": MessageLookupByLibrary.simpleMessage("编码"),
|
||||||
|
"error": MessageLookupByLibrary.simpleMessage("出错了"),
|
||||||
|
"exampleName": MessageLookupByLibrary.simpleMessage("名称示例"),
|
||||||
|
"experimentalFeature": MessageLookupByLibrary.simpleMessage("实验性功能"),
|
||||||
|
"export": MessageLookupByLibrary.simpleMessage("导出"),
|
||||||
|
"feedback": MessageLookupByLibrary.simpleMessage("反馈"),
|
||||||
|
"feedbackOnGithub":
|
||||||
|
MessageLookupByLibrary.simpleMessage("如果你有任何问题,请在GitHub反馈"),
|
||||||
|
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage("这些输入框不能为空。"),
|
||||||
|
"files": MessageLookupByLibrary.simpleMessage("文件"),
|
||||||
|
"foundNUpdate": m4,
|
||||||
|
"go": MessageLookupByLibrary.simpleMessage("开始"),
|
||||||
|
"goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"),
|
||||||
|
"host": MessageLookupByLibrary.simpleMessage("主机"),
|
||||||
|
"httpFailedWithCode": m5,
|
||||||
|
"import": MessageLookupByLibrary.simpleMessage("导入"),
|
||||||
|
"importAndExport": MessageLookupByLibrary.simpleMessage("导入或导出"),
|
||||||
|
"install": MessageLookupByLibrary.simpleMessage("安装"),
|
||||||
|
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"请先 https://docs.docker.com/engine/install docker"),
|
||||||
|
"invalidJson": MessageLookupByLibrary.simpleMessage("无效的json,存在格式问题"),
|
||||||
|
"invalidVersionHelp": m6,
|
||||||
|
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
|
||||||
|
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
|
||||||
|
"lastTry": MessageLookupByLibrary.simpleMessage("最后尝试"),
|
||||||
|
"launchPage": MessageLookupByLibrary.simpleMessage("启动页"),
|
||||||
|
"license": MessageLookupByLibrary.simpleMessage("开源证书"),
|
||||||
|
"loadingFiles": MessageLookupByLibrary.simpleMessage("正在加载目录。。。"),
|
||||||
|
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
|
||||||
|
"madeWithLove": m7,
|
||||||
|
"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或域名"),
|
||||||
|
"pingNoServer": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"没有服务器可用于Ping\n请在服务器tab添加服务器后再试"),
|
||||||
|
"platformNotSupportUpdate":
|
||||||
|
MessageLookupByLibrary.simpleMessage("当前平台不支持更新,请编译最新源码后手动安装"),
|
||||||
|
"plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"),
|
||||||
|
"plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"),
|
||||||
|
"port": MessageLookupByLibrary.simpleMessage("端口"),
|
||||||
|
"privateKey": MessageLookupByLibrary.simpleMessage("私钥"),
|
||||||
|
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
|
||||||
|
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
|
||||||
|
"reportBugsOnGithubIssue": m8,
|
||||||
|
"restore": MessageLookupByLibrary.simpleMessage("恢复"),
|
||||||
|
"restoreSuccess":
|
||||||
|
MessageLookupByLibrary.simpleMessage("恢复成功,需要重启App来应用更改"),
|
||||||
|
"restoreSureWithDate": 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,
|
||||||
|
"sureNoPwd": MessageLookupByLibrary.simpleMessage("确认使用无密码?"),
|
||||||
|
"sureToDeleteServer": m12,
|
||||||
|
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
|
||||||
|
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
||||||
|
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
|
||||||
|
"unkownConvertMode": MessageLookupByLibrary.simpleMessage("未知转换模式"),
|
||||||
|
"update": MessageLookupByLibrary.simpleMessage("更新"),
|
||||||
|
"updateAll": MessageLookupByLibrary.simpleMessage("更新全部"),
|
||||||
|
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"你设置为0,服务器状态不会自动刷新。\n你可以手动下拉刷新。"),
|
||||||
|
"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("更改将会立即生效")
|
||||||
|
};
|
||||||
|
}
|
||||||
1370
lib/generated/l10n.dart
Normal file
131
lib/l10n/intl_en.arb
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"server": "Server",
|
||||||
|
"convert": "Convert",
|
||||||
|
"ping": "Ping",
|
||||||
|
"debug": "Debug",
|
||||||
|
"addAServer": "add a server",
|
||||||
|
"setting": "Setting",
|
||||||
|
"license": "License",
|
||||||
|
"snippet": "Snippet",
|
||||||
|
"privateKey": "Private Key",
|
||||||
|
"madeWithLove": "\nMade with ❤️ by {myGithub}",
|
||||||
|
"aboutThanks": "\nAll rights reserved.\n\nThanks to the following people who participated in the test.",
|
||||||
|
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
|
||||||
|
"serverTabLoading": "Loading...",
|
||||||
|
"serverTabPlzSave": "Please 'save' this private key again.",
|
||||||
|
"serverTabFailed": "Failed",
|
||||||
|
"serverTabUnkown": "Unknown state",
|
||||||
|
"serverTabConnecting": "Connecting...",
|
||||||
|
"decode": "Decode",
|
||||||
|
"encode": "Encode",
|
||||||
|
"currentMode": "Current Mode",
|
||||||
|
"unkownConvertMode": "Unknown convert mode",
|
||||||
|
"copy": "Copy",
|
||||||
|
"upsideDown": "Upside Down",
|
||||||
|
"pingAvg": "Avg:",
|
||||||
|
"unknown": "unknown",
|
||||||
|
"min": "min",
|
||||||
|
"max": "max",
|
||||||
|
"ms": "ms",
|
||||||
|
"ttl": "ttl",
|
||||||
|
"loss": "loss",
|
||||||
|
"noResult": "No result",
|
||||||
|
"pingInputIP": "Please input a target IP/domain.",
|
||||||
|
"clear": "Clear",
|
||||||
|
"start": "Start",
|
||||||
|
"appPrimaryColor": "App primary color",
|
||||||
|
"updateServerStatusInterval": "Server status update interval",
|
||||||
|
"willTakEeffectImmediately": "Will take effect immediately",
|
||||||
|
"launchPage": "Launch page",
|
||||||
|
"versionUpdated": "Current: v1.0.{build}, is up to date",
|
||||||
|
"versionUnknownUpdate": "Current: v1.0.{build}",
|
||||||
|
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
|
||||||
|
"second": "s",
|
||||||
|
"updateIntervalEqual0": "You set to 0, will not update automatically.\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.",
|
||||||
|
"sureNoPwd": "Are you sure to use no password?",
|
||||||
|
"plzSelectKey": "Please select a key.",
|
||||||
|
"exampleName": "Example name",
|
||||||
|
"stop": "Stop",
|
||||||
|
"download": "Download",
|
||||||
|
"copyPath": "Copy path",
|
||||||
|
"keepForeground": "Keep app foreground!",
|
||||||
|
"downloadFinished": "Download finished",
|
||||||
|
"downloadStatus": "{percent}% of {size}",
|
||||||
|
"sftpDlPrepare": "Preparing to connect...",
|
||||||
|
"sftpSSHConnected": "SFTP Connected",
|
||||||
|
"spentTime": "Spent time: {time}",
|
||||||
|
"backDir": "Back",
|
||||||
|
"alreadyLastDir": "Already in last directory.",
|
||||||
|
"open": "Open",
|
||||||
|
"sureDelete": "Are you sure to delete [{name}]?",
|
||||||
|
"containerStatus": "Container status",
|
||||||
|
"noClient": "No client",
|
||||||
|
"installDockerWithUrl": "Please https://docs.docker.com/engine/install docker first.",
|
||||||
|
"waitConnection": "Please wait for the connection to be established.",
|
||||||
|
"unknownError": "Unknown error",
|
||||||
|
"dockerStatusRunningFmt": "{count} container running.",
|
||||||
|
"dockerStatusRunningAndStoppedFmt": "{runningCount} running, {stoppedCount} container stopped.",
|
||||||
|
"install": "install",
|
||||||
|
"loadingFiles": "Loading files...",
|
||||||
|
"sftpNoDownloadTask": "No download task.",
|
||||||
|
"goSftpDlPage": "Go to SFTP download page?",
|
||||||
|
"createFolder": "Create folder",
|
||||||
|
"createFile": "Create file",
|
||||||
|
"rename": "Rename",
|
||||||
|
"dl2Local": "Download [{fileName}] to local?",
|
||||||
|
"error": "Error",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"files": "Files",
|
||||||
|
"experimentalFeature": "Experimental feature",
|
||||||
|
"reportBugsOnGithubIssue": "Please report bugs on {url}",
|
||||||
|
"noUpdateAvailable": "No update available",
|
||||||
|
"foundNUpdate": "Found {count} update",
|
||||||
|
"updateAll": "Update all",
|
||||||
|
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
|
||||||
|
"invalidVersionHelp": "Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don't have the above issues, please submit an issue on {url}.",
|
||||||
|
"noInterface": "No interface",
|
||||||
|
"lastTry": "Last try!",
|
||||||
|
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
|
||||||
|
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).",
|
||||||
|
"backup": "Backup",
|
||||||
|
"restore": "Restore",
|
||||||
|
"restoreSureWithDate": "Are you sure to restore from {date} ?",
|
||||||
|
"backupVersionNotMatch": "Backup version is not match.",
|
||||||
|
"invalidJson": "Invalid JSON",
|
||||||
|
"restoreSuccess": "Restore success. Restart app to apply.",
|
||||||
|
"clickSee": "Click here",
|
||||||
|
"feedback": "Feedback",
|
||||||
|
"feedbackOnGithub": "If you have any questions, please feedback on Github.",
|
||||||
|
"update": "Update"
|
||||||
|
}
|
||||||
131
lib/l10n/intl_zh.arb
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"server": "服务器",
|
||||||
|
"convert": "转换",
|
||||||
|
"ping": "Ping",
|
||||||
|
"debug": "调试",
|
||||||
|
"addAServer": "添加服务器",
|
||||||
|
"setting": "设置",
|
||||||
|
"license": "开源证书",
|
||||||
|
"snippet": "代码片段",
|
||||||
|
"privateKey": "私钥",
|
||||||
|
"madeWithLove": "\n用❤️制作 by {myGithub}",
|
||||||
|
"aboutThanks": "\n保留所有权利。\n\n感谢以下参与软件测试的各位。",
|
||||||
|
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
|
||||||
|
"serverTabLoading": "加载中...",
|
||||||
|
"serverTabPlzSave": "请再次保存该私钥",
|
||||||
|
"serverTabFailed": "失败",
|
||||||
|
"serverTabUnkown": "未知状态",
|
||||||
|
"serverTabConnecting": "连接中...",
|
||||||
|
"decode": "解码",
|
||||||
|
"encode": "编码",
|
||||||
|
"currentMode": "当前模式",
|
||||||
|
"unkownConvertMode": "未知转换模式",
|
||||||
|
"copy": "复制到剪切板",
|
||||||
|
"upsideDown": "上下交换",
|
||||||
|
"pingAvg": "平均:",
|
||||||
|
"unknown": "未知",
|
||||||
|
"min": "最小",
|
||||||
|
"max": "最大",
|
||||||
|
"ms": "毫秒",
|
||||||
|
"ttl": "缓存时间",
|
||||||
|
"loss": "丢包率",
|
||||||
|
"noResult": "无结果",
|
||||||
|
"pingInputIP": "请输入目标IP或域名",
|
||||||
|
"clear": "清除",
|
||||||
|
"start": "开始",
|
||||||
|
"appPrimaryColor": "App主要色",
|
||||||
|
"updateServerStatusInterval": "服务器状态刷新间隔",
|
||||||
|
"willTakEeffectImmediately": "更改将会立即生效",
|
||||||
|
"launchPage": "启动页",
|
||||||
|
"versionUpdated": "当前:v1.0.{build}, 已是最新版本",
|
||||||
|
"versionUnknownUpdate": "当前:v1.0.{build}",
|
||||||
|
"versionHaveUpdate": "找到新版本:v1.0.{build}, 点击更新",
|
||||||
|
"second": "秒",
|
||||||
|
"updateIntervalEqual0": "你设置为0,服务器状态不会自动刷新。\n你可以手动下拉刷新。",
|
||||||
|
"edit": "编辑",
|
||||||
|
"noSavedPrivateKey": "没有已保存的私钥。",
|
||||||
|
"name": "名称",
|
||||||
|
"pwd": "密码",
|
||||||
|
"save": "保存",
|
||||||
|
"delete": "删除",
|
||||||
|
"fieldMustNotEmpty": "这些输入框不能为空。",
|
||||||
|
"importAndExport": "导入或导出",
|
||||||
|
"choose": "选择",
|
||||||
|
"import": "导入",
|
||||||
|
"export": "导出",
|
||||||
|
"ok": "好",
|
||||||
|
"cancel": "取消",
|
||||||
|
"urlOrJson": "链接或JSON",
|
||||||
|
"go": "开始",
|
||||||
|
"httpFailedWithCode": "请求失败, 状态码: {code}",
|
||||||
|
"run": "运行",
|
||||||
|
"noSavedSnippet": "没有已保存的代码片段。",
|
||||||
|
"chooseDestination": "选择目标",
|
||||||
|
"noServerAvailable": "没有可用的服务器。",
|
||||||
|
"result": "结果",
|
||||||
|
"close": "关闭",
|
||||||
|
"attention": "注意",
|
||||||
|
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
|
||||||
|
"host": "主机",
|
||||||
|
"port": "端口",
|
||||||
|
"user": "用户",
|
||||||
|
"keyAuth": "公钥认证",
|
||||||
|
"addPrivateKey": "添加一个私钥",
|
||||||
|
"choosePrivateKey": "选择私钥",
|
||||||
|
"plzEnterHost": "请输入主机",
|
||||||
|
"sureNoPwd": "确认使用无密码?",
|
||||||
|
"plzSelectKey": "请选择私钥",
|
||||||
|
"exampleName": "名称示例",
|
||||||
|
"stop": "停止",
|
||||||
|
"download": "下载",
|
||||||
|
"copyPath": "复制路径",
|
||||||
|
"keepForeground": "请保持应用处于前台!",
|
||||||
|
"downloadFinished": "下载完成!",
|
||||||
|
"downloadStatus": "{size} 的 {percent}%",
|
||||||
|
"sftpDlPrepare": "准备连接至服务器...",
|
||||||
|
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
|
||||||
|
"spentTime": "耗时: {time}",
|
||||||
|
"backDir": "返回上一级",
|
||||||
|
"alreadyLastDir": "已经是最上层目录了",
|
||||||
|
"open": "打开",
|
||||||
|
"sureDelete": "确定删除[{name}]?",
|
||||||
|
"containerStatus": "容器状态",
|
||||||
|
"noClient": "没有SSH连接",
|
||||||
|
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
|
||||||
|
"waitConnection": "请等待连接建立",
|
||||||
|
"unknownError": "未知错误",
|
||||||
|
"dockerStatusRunningFmt": "{count}个容器正在运行",
|
||||||
|
"dockerStatusRunningAndStoppedFmt": "{runningCount}个正在运行, {stoppedCount}个已停止",
|
||||||
|
"install": "安装",
|
||||||
|
"loadingFiles": "正在加载目录。。。",
|
||||||
|
"sftpNoDownloadTask": "没有下载任务",
|
||||||
|
"goSftpDlPage": "前往下载页?",
|
||||||
|
"createFolder": "创建文件夹",
|
||||||
|
"createFile": "创建文件",
|
||||||
|
"rename": "重命名",
|
||||||
|
"dl2Local": "下载 [{fileName}] 到本地?",
|
||||||
|
"error": "出错了",
|
||||||
|
"disconnected": "连接断开",
|
||||||
|
"files": "文件",
|
||||||
|
"experimentalFeature": "实验性功能",
|
||||||
|
"reportBugsOnGithubIssue": "请到 {url} 提交问题",
|
||||||
|
"noUpdateAvailable": "没有可用更新",
|
||||||
|
"foundNUpdate": "找到 {count} 个更新",
|
||||||
|
"updateAll": "更新全部",
|
||||||
|
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
|
||||||
|
"invalidVersionHelp": "请确保正确安装了docker,或者使用的非自编译版本。如果没有以上问题,请在 {url} 提交问题。",
|
||||||
|
"noInterface": "没有可用的接口",
|
||||||
|
"lastTry": "最后尝试",
|
||||||
|
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
|
||||||
|
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。\n除了设置项,恢复的数据不会覆盖现有数据。",
|
||||||
|
"backup": "备份",
|
||||||
|
"restore": "恢复",
|
||||||
|
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
|
||||||
|
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
|
||||||
|
"invalidJson": "无效的json,存在格式问题",
|
||||||
|
"restoreSuccess": "恢复成功,需要重启App来应用更改",
|
||||||
|
"clickSee": "点击查看",
|
||||||
|
"feedback": "反馈",
|
||||||
|
"feedbackOnGithub": "如果你有任何问题,请在GitHub反馈",
|
||||||
|
"update": "更新"
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
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/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';
|
||||||
@@ -18,10 +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(SnippetProvider());
|
||||||
locator.registerSingleton(PrivateKeyProvider());
|
locator.registerSingleton(PrivateKeyProvider());
|
||||||
|
locator.registerSingleton(SftpDownloadProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setupLocatorForStores() async {
|
Future<void> setupLocatorForStores() async {
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ 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/data/provider/snippet.dart';
|
||||||
import 'package:toolbox/locator.dart';
|
import 'package:toolbox/locator.dart';
|
||||||
|
|
||||||
@@ -62,10 +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<SnippetProvider>()),
|
||||||
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
|
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (_) => locator<SftpDownloadProvider>()),
|
||||||
],
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
|
|||||||
252
lib/view/page/apt.dart
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
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 textController = TextEditingController();
|
||||||
|
final _aptProvider = locator<AptProvider>();
|
||||||
|
late S s;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_media = MediaQuery.of(context);
|
||||||
|
s = S.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
Function onSubmitted = () {
|
||||||
|
if (textController.text == '') {
|
||||||
|
showRoundDialog(context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok)),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ignore: prefer_function_declarations_over_variables
|
||||||
|
PwdRequestFunc onPwdRequest = (triedTimes, user) async {
|
||||||
|
if (!mounted) return '';
|
||||||
|
await showRoundDialog(
|
||||||
|
context,
|
||||||
|
triedTimes == 3 ? s.lastTry : (user ?? s.unknown),
|
||||||
|
TextField(
|
||||||
|
controller: textController,
|
||||||
|
keyboardType: TextInputType.visiblePassword,
|
||||||
|
obscureText: true,
|
||||||
|
onSubmitted: (_) => onSubmitted(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: s.pwd,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(s.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => onSubmitted(),
|
||||||
|
child: Text(
|
||||||
|
s.ok,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
return textController.text.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
_aptProvider.init(
|
||||||
|
si.client!,
|
||||||
|
si.status.sysVer.dist,
|
||||||
|
() =>
|
||||||
|
scrollController.jumpTo(scrollController.position.maxScrollExtent),
|
||||||
|
() => scrollControllerUpdate
|
||||||
|
.jumpTo(scrollControllerUpdate.positions.last.maxScrollExtent),
|
||||||
|
onPwdRequest);
|
||||||
|
_aptProvider.refreshInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: _media.size.height * 0.4,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(17),
|
||||||
|
child: RoundRectCard(
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(17),
|
||||||
|
child: Text(
|
||||||
|
apt.error!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (apt.updateLog == null && apt.upgradeable == null) {
|
||||||
|
return centerLoading;
|
||||||
|
}
|
||||||
|
if (apt.updateLog != null && apt.upgradeable == null) {
|
||||||
|
return SizedBox(
|
||||||
|
height: _media.size.height -
|
||||||
|
_media.padding.top -
|
||||||
|
_media.padding.bottom,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
215
lib/view/page/backup.dart
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:toolbox/core/extension/colorx.dart';
|
||||||
|
import 'package:toolbox/core/utils.dart';
|
||||||
|
import 'package:toolbox/data/model/app/backup.dart';
|
||||||
|
import 'package:toolbox/data/res/color.dart';
|
||||||
|
import 'package:toolbox/data/res/font_style.dart';
|
||||||
|
import 'package:toolbox/data/store/private_key.dart';
|
||||||
|
import 'package:toolbox/data/store/server.dart';
|
||||||
|
import 'package:toolbox/data/store/setting.dart';
|
||||||
|
import 'package:toolbox/data/store/snippet.dart';
|
||||||
|
import 'package:toolbox/generated/l10n.dart';
|
||||||
|
import 'package:toolbox/locator.dart';
|
||||||
|
|
||||||
|
const backupFormatVersion = 1;
|
||||||
|
|
||||||
|
class BackupPage extends StatelessWidget {
|
||||||
|
BackupPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
final setting = locator<SettingStore>();
|
||||||
|
final server = locator<ServerStore>();
|
||||||
|
final snippet = locator<SnippetStore>();
|
||||||
|
final privateKey = locator<PrivateKeyStore>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final media = MediaQuery.of(context);
|
||||||
|
final s = S.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(s.importAndExport, style: size18),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(37),
|
||||||
|
child: Text(
|
||||||
|
s.backupTip,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 107,
|
||||||
|
),
|
||||||
|
_buildCard(s.restore, Icons.download, media,
|
||||||
|
() => _showImportDialog(context, s)),
|
||||||
|
const SizedBox(height: 7),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 7),
|
||||||
|
_buildCard(s.backup, Icons.file_upload, media,
|
||||||
|
() => _showExportDialog(context, s))
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCard(String text, IconData icon, MediaQueryData media,
|
||||||
|
FutureOr Function() onTap) {
|
||||||
|
final priColor = primaryColor;
|
||||||
|
final textColor = priColor.isBrightColor ? Colors.black : Colors.white;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(37), color: priColor),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 7),
|
||||||
|
Text(text, style: TextStyle(color: textColor)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showExportDialog(BuildContext context, S s) async {
|
||||||
|
final exportFieldController = TextEditingController()
|
||||||
|
..text = _diyEncrtpt(json.encode(Backup(
|
||||||
|
backupFormatVersion,
|
||||||
|
DateTime.now().toString().split('.').first,
|
||||||
|
server.fetch(),
|
||||||
|
snippet.fetch(),
|
||||||
|
privateKey.fetch(),
|
||||||
|
setting.primaryColor.fetch() ?? Colors.pinkAccent.value,
|
||||||
|
setting.serverStatusUpdateInterval.fetch() ?? 2,
|
||||||
|
setting.launchPage.fetch() ?? 0,
|
||||||
|
)));
|
||||||
|
await showRoundDialog(
|
||||||
|
context,
|
||||||
|
s.export,
|
||||||
|
TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'JSON',
|
||||||
|
),
|
||||||
|
maxLines: 7,
|
||||||
|
controller: exportFieldController,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
child: Text(s.copy),
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(text: exportFieldController.text));
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showImportDialog(BuildContext context, S s) async {
|
||||||
|
final importFieldController = TextEditingController();
|
||||||
|
await showRoundDialog(
|
||||||
|
context,
|
||||||
|
s.import,
|
||||||
|
TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'JSON',
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
controller: importFieldController,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async =>
|
||||||
|
await _import(importFieldController.text.trim(), context, s),
|
||||||
|
child: const Text('GO'),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _import(String text, BuildContext context, S s) async {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_importBackup(text, context, s);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _importBackup(String raw, BuildContext context, S s) async {
|
||||||
|
try {
|
||||||
|
final backup = await compute(_decode, raw);
|
||||||
|
if (backupFormatVersion != backup.version) {
|
||||||
|
showSnackBar(context, Text(s.backupVersionNotMatch));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await showRoundDialog(
|
||||||
|
context, s.attention, Text(s.restoreSureWithDate(backup.date)), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
for (final s in backup.snippets) {
|
||||||
|
snippet.put(s);
|
||||||
|
}
|
||||||
|
for (final s in backup.spis) {
|
||||||
|
server.put(s);
|
||||||
|
}
|
||||||
|
for (final s in backup.keys) {
|
||||||
|
privateKey.put(s);
|
||||||
|
}
|
||||||
|
setting.primaryColor.put(backup.primaryColor);
|
||||||
|
setting.serverStatusUpdateInterval
|
||||||
|
.put(backup.serverStatusUpdateInterval);
|
||||||
|
setting.launchPage.put(backup.launchPage);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showSnackBar(context, Text(s.restoreSuccess));
|
||||||
|
},
|
||||||
|
child: Text(s.ok),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
showSnackBar(context, Text(s.invalidJson));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Backup _decode(String raw) {
|
||||||
|
final decrypted = _diyDecrypt(raw);
|
||||||
|
return Backup.fromJson(json.decode(decrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
String _diyEncrtpt(String raw) =>
|
||||||
|
json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
|
||||||
|
String _diyDecrypt(String raw) {
|
||||||
|
final list = json.decode(raw);
|
||||||
|
final sb = StringBuffer();
|
||||||
|
for (final e in list) {
|
||||||
|
sb.writeCharCode((e - 1) ~/ 2);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ import 'package:clipboard/clipboard.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:toolbox/core/utils.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
|
||||||
@@ -48,6 +47,7 @@ class _ConvertPageState extends State<ConvertPage>
|
|||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||||
|
controller: ScrollController(),
|
||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
const SizedBox(height: 13),
|
const SizedBox(height: 13),
|
||||||
_buildInputTop(),
|
_buildInputTop(),
|
||||||
@@ -64,7 +64,7 @@ class _ConvertPageState extends State<ConvertPage>
|
|||||||
showSnackBar(context, Text('Error: \n$e'));
|
showSnackBar(context, Text('Error: \n$e'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: 'convert',
|
tooltip: s.convert,
|
||||||
child: const Icon(Icons.send),
|
child: const Icon(Icons.send),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -82,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(
|
||||||
@@ -103,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;
|
||||||
@@ -113,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 == ''
|
||||||
? ' '
|
? ' '
|
||||||
@@ -127,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,
|
||||||
@@ -151,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(),
|
||||||
),
|
),
|
||||||
@@ -161,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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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, widget.spi.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,32 @@
|
|||||||
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/services.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/analysis.dart';
|
||||||
import 'package:toolbox/core/build_mode.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/icon/common.dart';
|
||||||
|
import 'package:toolbox/data/res/tab.dart';
|
||||||
import 'package:toolbox/data/res/url.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/backup.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/ping.dart';
|
||||||
import 'package:toolbox/view/page/private_key/list.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/page/snippet/list.dart';
|
||||||
import 'package:toolbox/view/widget/url_text.dart';
|
import 'package:toolbox/view/widget/url_text.dart';
|
||||||
|
|
||||||
@@ -33,28 +44,41 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
SingleTickerProviderStateMixin,
|
SingleTickerProviderStateMixin,
|
||||||
AfterLayoutMixin,
|
AfterLayoutMixin,
|
||||||
WidgetsBindingObserver {
|
WidgetsBindingObserver {
|
||||||
final List<String> _tabs = ['Servers', 'En/Decode'];
|
|
||||||
late final TabController _tabController;
|
|
||||||
late final ServerProvider _serverProvider;
|
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();
|
||||||
_serverProvider = locator<ServerProvider>();
|
_serverProvider = locator<ServerProvider>();
|
||||||
WidgetsBinding.instance?.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_tabController = TabController(length: _tabs.length, vsync: 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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
WidgetsBinding.instance?.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
super.didChangeAppLifecycleState(state);
|
super.didChangeAppLifecycleState(state);
|
||||||
if (state == AppLifecycleState.paused) {
|
if (state == AppLifecycleState.paused) {
|
||||||
|
_serverProvider.setDisconnected();
|
||||||
_serverProvider.stopAutoRefresh();
|
_serverProvider.stopAutoRefresh();
|
||||||
}
|
}
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
@@ -64,72 +88,213 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
|
|
||||||
@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(BuildData.name),
|
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(BuildData.name),
|
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(), 'private key list')
|
),
|
||||||
.go(context),
|
ListTile(
|
||||||
),
|
leading: const Icon(Icons.vpn_key),
|
||||||
ListTile(
|
title: Text(s.privateKey),
|
||||||
leading: const Icon(Icons.snippet_folder),
|
onTap: () => AppRoute(
|
||||||
title: const Text('Snippet'),
|
const StoredPrivateKeysPage(), 'private key list')
|
||||||
onTap: () =>
|
.go(context),
|
||||||
AppRoute(const SnippetListPage(), 'snippet list').go(context),
|
),
|
||||||
),
|
ListTile(
|
||||||
AboutListTile(
|
leading: const Icon(Icons.download),
|
||||||
icon: const Icon(Icons.text_snippet),
|
title: Text(s.download),
|
||||||
child: const Text('Licences'),
|
onTap: () =>
|
||||||
applicationName: BuildData.name,
|
AppRoute(const SFTPDownloadedPage(), 'snippet list')
|
||||||
applicationVersion: _buildVersionStr(),
|
.go(context),
|
||||||
applicationIcon: _buildIcon(),
|
),
|
||||||
aboutBoxChildren: const [
|
ListTile(
|
||||||
UrlText(
|
leading: const Icon(Icons.import_export),
|
||||||
text: '\nMade with ❤️ by $myGithub', replace: 'LollipopKit'),
|
title: Text(s.backup),
|
||||||
UrlText(
|
onTap: () =>
|
||||||
text:
|
AppRoute(BackupPage(), 'backup page').go(context),
|
||||||
'\nThanks $rainSunMeGithub for participating in the test.\n\nAll rights reserved.',
|
),
|
||||||
replace: 'RainSunMe',
|
ListTile(
|
||||||
),
|
leading: const Icon(Icons.info),
|
||||||
],
|
title: Text(s.feedback),
|
||||||
|
onTap: () => showRoundDialog(
|
||||||
|
context, s.feedback, Text(s.feedbackOnGithub), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Clipboard.setData(
|
||||||
|
const ClipboardData(text: issueUrl)),
|
||||||
|
child: Text(s.copy)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => openUrl(issueUrl),
|
||||||
|
child: Text(s.feedback)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.close))
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.snippet_folder),
|
||||||
|
title: Text(s.snippet),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
const UrlText(
|
||||||
|
text: rainSunMeGithub,
|
||||||
|
replace: 'RainSunMe',
|
||||||
|
),
|
||||||
|
const UrlText(
|
||||||
|
text: fectureGithub,
|
||||||
|
replace: 'fecture',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: Text(s.license),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -137,14 +302,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: appIcon,
|
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
|
||||||
|
|||||||
180
lib/view/page/ping.dart
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
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 = [];
|
||||||
|
final _serverProvider = locator<ServerProvider>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_serverProvider.servers.isEmpty) {
|
||||||
|
showSnackBar(context, Text(s.pingNoServer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait(_serverProvider.servers.map((e) async {
|
||||||
|
if (e.client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = await e.client!.run('ping -c 3 $target').string;
|
||||||
|
_results.add(PingResult.parse(e.info.name, result));
|
||||||
|
setState(() {});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import 'package:flutter/material.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/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';
|
||||||
|
|
||||||
@@ -22,9 +24,15 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
|||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
final keyController = TextEditingController();
|
final keyController = TextEditingController();
|
||||||
final pwdController = TextEditingController();
|
final pwdController = TextEditingController();
|
||||||
|
final nameNode = FocusNode();
|
||||||
|
final keyNode = FocusNode();
|
||||||
|
final pwdNode = FocusNode();
|
||||||
|
|
||||||
|
late FocusScopeNode focusScope;
|
||||||
|
|
||||||
late PrivateKeyProvider _provider;
|
late PrivateKeyProvider _provider;
|
||||||
late Widget loading;
|
late Widget loading;
|
||||||
|
late S s;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -33,12 +41,20 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
|||||||
loading = const SizedBox();
|
loading = const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
s = S.of(context);
|
||||||
|
focusScope = FocusScope.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();
|
||||||
@@ -52,7 +68,9 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
|||||||
TextField(
|
TextField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
decoration: buildDecoration('Name', icon: Icons.info),
|
focusNode: nameNode,
|
||||||
|
onSubmitted: (_) => focusScope.requestFocus(keyNode),
|
||||||
|
decoration: buildDecoration(s.name, icon: Icons.info),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: keyController,
|
controller: keyController,
|
||||||
@@ -60,29 +78,31 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
|||||||
minLines: 3,
|
minLines: 3,
|
||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
|
focusNode: keyNode,
|
||||||
|
onSubmitted: (_) => focusScope.requestFocus(pwdNode),
|
||||||
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,
|
||||||
|
focusNode: pwdNode,
|
||||||
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),
|
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
||||||
loading
|
loading
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.save),
|
tooltip: s.save,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final name = nameController.text;
|
final name = nameController.text;
|
||||||
final key = keyController.text;
|
final key = keyController.text.trim();
|
||||||
final pwd = pwdController.text;
|
final pwd = pwdController.text;
|
||||||
if (name.isEmpty || key.isEmpty || pwd.isEmpty) {
|
if (name.isEmpty || key.isEmpty) {
|
||||||
showSnackBar(
|
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||||
context, const Text('Three fields must not be empty.'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
@@ -114,6 +134,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
|||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
child: const Icon(Icons.save),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
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/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/font_style.dart';
|
||||||
import 'package:toolbox/data/res/icon/linux_icons.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 style11 = TextStyle(fontSize: 11);
|
||||||
@@ -23,11 +27,13 @@ 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
|
||||||
@@ -41,10 +47,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
Widget _buildMainPage(ServerInfo si) {
|
Widget _buildMainPage(ServerInfo si) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(si.info.name),
|
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),
|
||||||
@@ -68,41 +74,44 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
|
|
||||||
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.iowait, 'io'),
|
||||||
),
|
SizedBox(
|
||||||
_buildCPUTimePercent(ss.cpu2Status.idle, 'idle')
|
width: _media.size.width * 0.03,
|
||||||
],
|
),
|
||||||
)
|
_buildCPUTimePercent(ss.cpu2Status.idle, 'idle')
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
_buildCPUProgress(ss)
|
],
|
||||||
]),
|
),
|
||||||
|
_buildCPUProgress(ss)
|
||||||
|
]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -114,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,
|
||||||
),
|
),
|
||||||
@@ -146,6 +155,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProgress(double percent) {
|
Widget _buildProgress(double percent) {
|
||||||
|
if (percent > 100) percent = 100;
|
||||||
final pColor = primaryColor;
|
final pColor = primaryColor;
|
||||||
final percentWithinOne = percent / 100;
|
final percentWithinOne = percent / 100;
|
||||||
return LinearProgressIndicator(
|
return LinearProgressIndicator(
|
||||||
@@ -158,7 +168,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
|
|
||||||
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: [
|
||||||
@@ -173,65 +183,56 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
String convertMB(int mb) {
|
|
||||||
const suffix = ['MB', 'GB', 'TB'];
|
|
||||||
double value = mb.toDouble();
|
|
||||||
int squareTimes = 0;
|
|
||||||
for (; value / 1024 > 1 && squareTimes < 3; squareTimes++) {
|
|
||||||
value /= 1024;
|
|
||||||
}
|
|
||||||
var finalValue = value.toStringAsFixed(1);
|
|
||||||
if (finalValue.endsWith('.0')) {
|
|
||||||
finalValue = finalValue.replaceFirst('.0', '');
|
|
||||||
}
|
|
||||||
return '$finalValue ${suffix[squareTimes]}';
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMemView(ServerStatus ss) {
|
Widget _buildMemView(ServerStatus ss) {
|
||||||
final pColor = primaryColor;
|
final pColor = primaryColor;
|
||||||
final used = ss.memory.used / ss.memory.total;
|
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;
|
||||||
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(convertMB(ss.memory.used), pColor),
|
Row(
|
||||||
_buildMemExplain(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
convertMB(ss.memory.cache), pColor.withAlpha(77)),
|
children: [
|
||||||
_buildMemExplain(convertMB(ss.memory.total - ss.memory.used),
|
_buildMemExplain((ss.memory.used * mb).convertBytes, pColor),
|
||||||
progressColor.resolve(context))
|
_buildMemExplain(
|
||||||
],
|
(ss.memory.cache * mb).convertBytes, pColor.withAlpha(77)),
|
||||||
),
|
_buildMemExplain(
|
||||||
const SizedBox(
|
((ss.memory.total - ss.memory.used) * mb).convertBytes,
|
||||||
height: 7,
|
progressColor.resolve(context))
|
||||||
),
|
],
|
||||||
Row(
|
),
|
||||||
children: [
|
const SizedBox(
|
||||||
SizedBox(
|
height: 7,
|
||||||
width: width * used,
|
),
|
||||||
|
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),
|
||||||
// memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
|
),
|
||||||
value: ss.memory.total == 1
|
)
|
||||||
? 0
|
],
|
||||||
: ss.memory.cache / (ss.memory.total - ss.memory.used),
|
)
|
||||||
backgroundColor: progressColor.resolve(context),
|
],
|
||||||
color: pColor.withAlpha(77),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -265,7 +266,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) {
|
||||||
@@ -300,9 +301,19 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
height: 7,
|
height: 7,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
|
if (ns.devices.isEmpty) {
|
||||||
|
children.add(Center(
|
||||||
|
child: Text(
|
||||||
|
s.noInterface,
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
|
||||||
|
}
|
||||||
|
|
||||||
return RoundRectCard(Padding(
|
return RoundRectCard(Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: children,
|
children: children,
|
||||||
),
|
),
|
||||||
@@ -319,8 +330,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
Icons.device_hub,
|
Icons.device_hub,
|
||||||
size: 17,
|
size: 17,
|
||||||
),
|
),
|
||||||
|
Icon(Icons.arrow_downward, size: 17),
|
||||||
Icon(Icons.arrow_upward, size: 17),
|
Icon(Icons.arrow_upward, size: 17),
|
||||||
Icon(Icons.arrow_downward, size: 17)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import 'package:toolbox/data/model/server/server_private_info.dart';
|
|||||||
import 'package:toolbox/data/provider/private_key.dart';
|
import 'package:toolbox/data/provider/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';
|
||||||
@@ -28,13 +31,21 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
final usernameController = TextEditingController();
|
final usernameController = TextEditingController();
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
final keyController = TextEditingController();
|
final keyController = TextEditingController();
|
||||||
|
final nameFocus = FocusNode();
|
||||||
|
final ipFocus = FocusNode();
|
||||||
|
final portFocus = FocusNode();
|
||||||
|
final usernameFocus = FocusNode();
|
||||||
|
|
||||||
|
late FocusScopeNode focusScope;
|
||||||
|
|
||||||
late ServerProvider _serverProvider;
|
late ServerProvider _serverProvider;
|
||||||
|
|
||||||
|
late S s;
|
||||||
|
|
||||||
bool usePublicKey = false;
|
bool usePublicKey = false;
|
||||||
|
|
||||||
int _pubKeyIndex = -1;
|
int? _pubKeyIndex;
|
||||||
late PrivateKeyInfo _keyInfo;
|
PrivateKeyInfo? _keyInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -42,33 +53,36 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
_serverProvider = locator<ServerProvider>();
|
_serverProvider = locator<ServerProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
s = S.of(context);
|
||||||
|
focusScope = FocusScope.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: () {
|
||||||
showRoundDialog(
|
showRoundDialog(context, s.attention,
|
||||||
context,
|
Text(s.sureToDeleteServer(widget.spi!.name)), [
|
||||||
'Attention',
|
TextButton(
|
||||||
Text(
|
onPressed: () {
|
||||||
'Are you sure to delete server [${widget.spi!.name}]'),
|
_serverProvider.delServer(widget.spi!);
|
||||||
[
|
Navigator.of(context).pop();
|
||||||
TextButton(
|
Navigator.of(context).pop();
|
||||||
onPressed: () {
|
},
|
||||||
_serverProvider.delServer(widget.spi!);
|
child: Text(
|
||||||
Navigator.of(context).pop();
|
s.ok,
|
||||||
Navigator.of(context).pop();
|
style: const TextStyle(color: Colors.red),
|
||||||
},
|
)),
|
||||||
child: const Text(
|
TextButton(
|
||||||
'Yes',
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
style: TextStyle(color: Colors.red),
|
child: Text(s.cancel))
|
||||||
)),
|
]);
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('No'))
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete))
|
icon: const Icon(Icons.delete))
|
||||||
: const SizedBox()
|
: const SizedBox()
|
||||||
@@ -82,35 +96,42 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
decoration:
|
focusNode: nameFocus,
|
||||||
buildDecoration('Name', icon: Icons.info, hint: 'Example'),
|
onSubmitted: (_) => focusScope.requestFocus(ipFocus),
|
||||||
|
decoration: buildDecoration(s.name,
|
||||||
|
icon: Icons.info, hint: s.exampleName),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: ipController,
|
controller: ipController,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
|
onSubmitted: (_) => focusScope.requestFocus(portFocus),
|
||||||
|
focusNode: ipFocus,
|
||||||
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',
|
focusNode: portFocus,
|
||||||
|
onSubmitted: (_) => focusScope.requestFocus(usernameFocus),
|
||||||
|
decoration: buildDecoration(s.port,
|
||||||
icon: Icons.format_list_numbered, hint: '22'),
|
icon: Icons.format_list_numbered, hint: '22'),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: usernameController,
|
controller: usernameController,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
|
focusNode: usernameFocus,
|
||||||
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)),
|
||||||
@@ -121,8 +142,8 @@ 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(),
|
||||||
@@ -130,7 +151,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
? Consumer<PrivateKeyProvider>(builder: (_, key, __) {
|
? Consumer<PrivateKeyProvider>(builder: (_, key, __) {
|
||||||
for (var item in key.infos) {
|
for (var item in key.infos) {
|
||||||
if (item.id == widget.spi?.pubKeyId) {
|
if (item.id == widget.spi?.pubKeyId) {
|
||||||
_pubKeyIndex = key.infos.indexOf(item);
|
_pubKeyIndex ??= key.infos.indexOf(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final tiles = key.infos
|
final tiles = key.infos
|
||||||
@@ -142,7 +163,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
)
|
)
|
||||||
.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),
|
||||||
@@ -156,9 +177,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,
|
||||||
);
|
);
|
||||||
@@ -169,17 +190,31 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.send),
|
child: const Icon(Icons.send),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
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.'));
|
final cancel = await showRoundDialog<bool>(
|
||||||
return;
|
context,
|
||||||
|
s.attention,
|
||||||
|
Text(s.sureNoPwd),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text(s.ok)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: Text(s.cancel))
|
||||||
|
],
|
||||||
|
barrierDismiss: false);
|
||||||
|
if (cancel ?? true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (usePublicKey && _pubKeyIndex == -1) {
|
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 == '') {
|
||||||
@@ -188,19 +223,19 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
if (portController.text == '') {
|
if (portController.text == '') {
|
||||||
portController.text = '22';
|
portController.text = '22';
|
||||||
}
|
}
|
||||||
final authorization = usePublicKey
|
|
||||||
? {
|
if (widget.spi != null && widget.spi!.pubKeyId != null) {
|
||||||
"privateKey": _keyInfo.privateKey,
|
_keyInfo ??= locator<PrivateKeyStore>().get(widget.spi!.pubKeyId!);
|
||||||
"passphrase": _keyInfo.password
|
}
|
||||||
}
|
|
||||||
: passwordController.text;
|
final authorization = 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,
|
pwd: authorization,
|
||||||
pubKeyId: usePublicKey ? _keyInfo.id : null);
|
pubKeyId: usePublicKey ? _keyInfo!.id : null);
|
||||||
|
|
||||||
if (widget.spi == null) {
|
if (widget.spi == null) {
|
||||||
_serverProvider.addServer(spi);
|
_serverProvider.addServer(spi);
|
||||||
@@ -234,12 +269,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
ipController.text = widget.spi?.ip ?? '';
|
ipController.text = widget.spi?.ip ?? '';
|
||||||
portController.text = (widget.spi?.port ?? 22).toString();
|
portController.text = (widget.spi?.port ?? 22).toString();
|
||||||
usernameController.text = widget.spi?.user ?? '';
|
usernameController.text = widget.spi?.user ?? '';
|
||||||
if (widget.spi?.authorization is String) {
|
if (widget.spi?.pubKeyId == null) {
|
||||||
passwordController.text = widget.spi?.authorization as String? ?? '';
|
passwordController.text = widget.spi?.pwd ?? '';
|
||||||
} else {
|
} else {
|
||||||
final auth = widget.spi?.authorization as Map;
|
|
||||||
passwordController.text = auth['passphrase'];
|
|
||||||
keyController.text = auth['privateKey'];
|
|
||||||
usePublicKey = true;
|
usePublicKey = true;
|
||||||
}
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
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: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:pull_to_refresh/pull_to_refresh.dart';
|
||||||
import 'package:toolbox/core/route.dart';
|
import 'package:toolbox/core/route.dart';
|
||||||
|
import 'package:toolbox/core/utils.dart';
|
||||||
|
import 'package:toolbox/data/model/app/menu_item.dart';
|
||||||
import 'package:toolbox/data/model/server/server.dart';
|
import 'package:toolbox/data/model/server/server.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/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);
|
||||||
@@ -31,6 +39,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
late RefreshController _refreshController;
|
late RefreshController _refreshController;
|
||||||
|
|
||||||
late ServerProvider _serverProvider;
|
late ServerProvider _serverProvider;
|
||||||
|
late S s;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -45,6 +54,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
_media = MediaQuery.of(context);
|
_media = MediaQuery.of(context);
|
||||||
_theme = Theme.of(context);
|
_theme = Theme.of(context);
|
||||||
_primaryColor = primaryColor;
|
_primaryColor = primaryColor;
|
||||||
|
s = S.of(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -54,22 +64,27 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
|
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
|
||||||
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
|
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
|
||||||
if (pro.servers.isEmpty) {
|
if (pro.servers.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'There is no server.\nClick the fab to add one.',
|
s.serverTabEmpty,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SingleChildScrollView(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
padding: const EdgeInsets.all(7),
|
||||||
child: Column(
|
controller: ScrollController(),
|
||||||
children: [
|
itemBuilder: (ctx, idx) {
|
||||||
const SizedBox(height: 13),
|
if (idx == pro.servers.length) {
|
||||||
...pro.servers.map((e) => _buildEachServerCard(e)),
|
return SizedBox(height: _media.padding.bottom);
|
||||||
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: autoUpdate
|
body: autoUpdate
|
||||||
@@ -86,7 +101,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
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),
|
||||||
),
|
),
|
||||||
@@ -94,8 +109,8 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEachServerCard(ServerInfo si) {
|
Widget _buildEachServerCard(ServerInfo si) {
|
||||||
return Card(
|
return RoundRectCard(
|
||||||
child: InkWell(
|
InkWell(
|
||||||
onLongPress: () => AppRoute(
|
onLongPress: () => AppRoute(
|
||||||
ServerEditPage(
|
ServerEditPage(
|
||||||
spi: si.info,
|
spi: si.info,
|
||||||
@@ -105,7 +120,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
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: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'),
|
||||||
'server detail page')
|
'server detail page')
|
||||||
.go(context),
|
.go(context),
|
||||||
@@ -113,8 +128,8 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 == '/');
|
||||||
|
|
||||||
@@ -128,29 +143,39 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
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(
|
||||||
hasError
|
serverName,
|
||||||
? ConstrainedBox(
|
style: const TextStyle(
|
||||||
constraints: BoxConstraints(
|
fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
maxWidth: _media.size.width * 0.57, maxHeight: 15),
|
textScaleFactor: 1.0,
|
||||||
child: Marquee(
|
),
|
||||||
accelerationDuration: const Duration(seconds: 3),
|
const Icon(
|
||||||
accelerationCurve: Curves.linear,
|
Icons.keyboard_arrow_right,
|
||||||
decelerationDuration: const Duration(seconds: 3),
|
size: 17,
|
||||||
decelerationCurve: Curves.linear,
|
color: Colors.grey,
|
||||||
text: topRightStr,
|
|
||||||
textScaleFactor: 1.0,
|
|
||||||
style: style),
|
|
||||||
)
|
)
|
||||||
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
],
|
||||||
],
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
hasError
|
||||||
|
? GestureDetector(
|
||||||
|
onTap: () => showRoundDialog(
|
||||||
|
context, s.error, Text(ss.failedInfo ?? ''), []),
|
||||||
|
child: Text(s.clickSee, style: style))
|
||||||
|
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
||||||
|
_buildMoreBtn(spi),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 17,
|
height: 17,
|
||||||
@@ -160,10 +185,9 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
children: [
|
children: [
|
||||||
_buildPercentCircle(ss.cpu2Status.usedPercent()),
|
_buildPercentCircle(ss.cpu2Status.usedPercent()),
|
||||||
_buildPercentCircle(ss.memory.used / ss.memory.total * 100),
|
_buildPercentCircle(ss.memory.used / ss.memory.total * 100),
|
||||||
_buildIOData('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('Total:\n' + rootDisk.size,
|
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
|
||||||
'Used:\n' + rootDisk.usedPercent.toString() + '%')
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 13),
|
const SizedBox(height: 13),
|
||||||
@@ -175,11 +199,82 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
_buildExplainText('Net'),
|
_buildExplainText('Net'),
|
||||||
_buildExplainText('Disk'),
|
_buildExplainText('Disk'),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMoreBtn(ServerPrivateInfo spi) {
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2(
|
||||||
|
customButton: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 1.7),
|
||||||
|
child: Icon(
|
||||||
|
Icons.more_vert,
|
||||||
|
size: 17,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
customItemsIndexes: [ServerTabMenuItems.firstItems.length],
|
||||||
|
customItemsHeight: 8,
|
||||||
|
items: [
|
||||||
|
...ServerTabMenuItems.firstItems.map(
|
||||||
|
(item) => DropdownMenuItem<DropdownBtnItem>(
|
||||||
|
value: item,
|
||||||
|
child: item.build,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const DropdownMenuItem<Divider>(enabled: false, child: Divider()),
|
||||||
|
...ServerTabMenuItems.secondItems.map(
|
||||||
|
(item) => DropdownMenuItem<DropdownBtnItem>(
|
||||||
|
value: item,
|
||||||
|
child: item.build,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
final item = value as DropdownBtnItem;
|
||||||
|
switch (item) {
|
||||||
|
case ServerTabMenuItems.apt:
|
||||||
|
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
|
||||||
|
break;
|
||||||
|
case ServerTabMenuItems.sftp:
|
||||||
|
AppRoute(SFTPPage(spi), 'SFTP').go(context);
|
||||||
|
break;
|
||||||
|
case ServerTabMenuItems.snippet:
|
||||||
|
AppRoute(
|
||||||
|
SnippetListPage(
|
||||||
|
spi: spi,
|
||||||
|
),
|
||||||
|
'snippet list')
|
||||||
|
.go(context);
|
||||||
|
break;
|
||||||
|
case ServerTabMenuItems.edit:
|
||||||
|
AppRoute(
|
||||||
|
ServerEditPage(
|
||||||
|
spi: spi,
|
||||||
|
),
|
||||||
|
'Edit server info page')
|
||||||
|
.go(context);
|
||||||
|
break;
|
||||||
|
case ServerTabMenuItems.docker:
|
||||||
|
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemHeight: 37,
|
||||||
|
itemPadding: const EdgeInsets.only(left: 17, right: 17),
|
||||||
|
dropdownWidth: 160,
|
||||||
|
dropdownPadding: const EdgeInsets.symmetric(vertical: 7),
|
||||||
|
dropdownDecoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
),
|
||||||
|
dropdownElevation: 8,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildExplainText(String text) {
|
Widget _buildExplainText(String text) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _media.size.width * 0.2,
|
width: _media.size.width * 0.2,
|
||||||
@@ -196,21 +291,33 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
String? failedInfo) {
|
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:
|
||||||
if (failedInfo == null) {
|
if (failedInfo == null) {
|
||||||
return 'Failed';
|
return s.serverTabFailed;
|
||||||
}
|
}
|
||||||
if (failedInfo.contains('encypted')) {
|
if (failedInfo.contains('encypted')) {
|
||||||
return 'Please "save" this private key again.';
|
return s.serverTabPlzSave;
|
||||||
}
|
}
|
||||||
return failedInfo;
|
return failedInfo;
|
||||||
default:
|
default:
|
||||||
return 'Unknown State';
|
return s.serverTabUnkown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ 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:provider/provider.dart';
|
||||||
import 'package:toolbox/core/update.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/app.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/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';
|
||||||
|
|
||||||
@@ -20,15 +25,22 @@ 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);
|
static const textStyle = TextStyle(fontSize: 14);
|
||||||
late final ServerProvider _serverProvider;
|
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
|
||||||
@@ -36,6 +48,7 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_serverProvider = locator<ServerProvider>();
|
_serverProvider = locator<ServerProvider>();
|
||||||
_store = locator<SettingStore>();
|
_store = locator<SettingStore>();
|
||||||
|
_launchPageIdx = _store.launchPage.fetch()!;
|
||||||
_intervalValue = _store.serverStatusUpdateInterval.fetch()!.toDouble();
|
_intervalValue = _store.serverStatusUpdateInterval.fetch()!.toDouble();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,14 +56,15 @@ 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: [
|
||||||
_buildAppColorPreview(),
|
_buildAppColorPreview(),
|
||||||
_buildUpdateInterval(),
|
_buildUpdateInterval(),
|
||||||
_buildCheckUpdate()
|
_buildCheckUpdate(),
|
||||||
|
_buildLaunchPage()
|
||||||
].map((e) => RoundRectCard(e)).toList(),
|
].map((e) => RoundRectCard(e)).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -61,15 +75,15 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
String display;
|
String display;
|
||||||
if (app.newestBuild != null) {
|
if (app.newestBuild != null) {
|
||||||
if (app.newestBuild! > BuildData.build) {
|
if (app.newestBuild! > BuildData.build) {
|
||||||
display = 'Found: v1.0.${app.newestBuild}, click to update';
|
display = s.versionHaveUpdate(app.newestBuild!);
|
||||||
} else {
|
} else {
|
||||||
display = 'Current: v1.0.${BuildData.build},is up to date';
|
display = s.versionUpdated(BuildData.build);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
display = 'Current: v1.0.${BuildData.build}';
|
display = s.versionUnknownUpdate(BuildData.build);
|
||||||
}
|
}
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: roundRectCardPadding,
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
title: Text(
|
title: Text(
|
||||||
display,
|
display,
|
||||||
@@ -82,19 +96,19 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
|
|
||||||
Widget _buildUpdateInterval() {
|
Widget _buildUpdateInterval() {
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
tilePadding: EdgeInsets.zero,
|
tilePadding: roundRectCardPadding,
|
||||||
childrenPadding: EdgeInsets.zero,
|
childrenPadding: roundRectCardPadding,
|
||||||
textColor: priColor,
|
textColor: priColor,
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Server status update interval',
|
s.updateServerStatusInterval,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
),
|
),
|
||||||
subtitle: const Text(
|
subtitle: Text(
|
||||||
'Will take effect immediately.',
|
s.willTakEeffectImmediately,
|
||||||
style: TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
),
|
),
|
||||||
trailing: Text('${_intervalValue.toInt()} s'),
|
trailing: Text('${_intervalValue.toInt()} ${s.second}'),
|
||||||
children: [
|
children: [
|
||||||
Slider(
|
Slider(
|
||||||
thumbColor: priColor,
|
thumbColor: priColor,
|
||||||
@@ -111,16 +125,16 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
_store.serverStatusUpdateInterval.put(val.toInt());
|
_store.serverStatusUpdateInterval.put(val.toInt());
|
||||||
_serverProvider.startAutoRefresh();
|
_serverProvider.startAutoRefresh();
|
||||||
},
|
},
|
||||||
label: '${_intervalValue.toInt()} seconds',
|
label: '${_intervalValue.toInt()} ${s.second}',
|
||||||
divisions: 10,
|
divisions: 10,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 3,
|
height: 3,
|
||||||
),
|
),
|
||||||
_intervalValue == 0.0
|
_intervalValue == 0.0
|
||||||
? const Text(
|
? Text(
|
||||||
'You set to 0, will not update automatically.\nYou can pull to refresh manually.',
|
s.updateIntervalEqual0,
|
||||||
style: TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
)
|
)
|
||||||
: const SizedBox(),
|
: const SizedBox(),
|
||||||
@@ -134,12 +148,8 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
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,
|
||||||
@@ -147,10 +157,14 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
width: 27,
|
width: 27,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'App primary color',
|
s.appPrimaryColor,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
));
|
),
|
||||||
|
children: [
|
||||||
|
_buildAppColorPicker(priColor),
|
||||||
|
_buildColorPickerConfirmBtn()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAppColorPicker(Color selected) {
|
Widget _buildAppColorPicker(Color selected) {
|
||||||
@@ -171,4 +185,49 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildLaunchPage() {
|
||||||
|
return ExpansionTile(
|
||||||
|
textColor: priColor,
|
||||||
|
tilePadding: roundRectCardPadding,
|
||||||
|
childrenPadding: roundRectCardPadding,
|
||||||
|
title: Text(
|
||||||
|
s.launchPage,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
trailing: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
|
||||||
|
child: Text(
|
||||||
|
tabTitleName(context, _launchPageIdx),
|
||||||
|
style: textStyle,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: tabs
|
||||||
|
.map((e) => ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(
|
||||||
|
tabTitleName(context, tabs.indexOf(e)),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: _theme.textTheme.bodyText2!.color!.withAlpha(177)),
|
||||||
|
),
|
||||||
|
trailing: _buildRadio(tabs.indexOf(e)),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Radio _buildRadio(int index) {
|
||||||
|
return Radio<int>(
|
||||||
|
value: index,
|
||||||
|
groupValue: _launchPageIdx,
|
||||||
|
onChanged: (int? value) {
|
||||||
|
setState(() {
|
||||||
|
_launchPageIdx = value!;
|
||||||
|
_store.launchPage.put(value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
184
lib/view/page/sftp/downloaded.dart
Normal 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))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
lib/view/page/sftp/downloading.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:toolbox/core/extension/numx.dart';
|
||||||
|
import 'package:toolbox/core/utils.dart';
|
||||||
|
import 'package:toolbox/data/model/sftp/download_status.dart';
|
||||||
|
import 'package:toolbox/data/provider/sftp_download.dart';
|
||||||
|
import 'package:toolbox/data/res/font_style.dart';
|
||||||
|
import 'package:toolbox/generated/l10n.dart';
|
||||||
|
import 'package:toolbox/view/widget/center_loading.dart';
|
||||||
|
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||||
|
|
||||||
|
class SFTPDownloadingPage extends StatefulWidget {
|
||||||
|
const SFTPDownloadingPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SFTPDownloadingPageState createState() => _SFTPDownloadingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
|
||||||
|
late S s;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
s = S.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
s.download,
|
||||||
|
style: size18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: _buildBody(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
|
return Consumer<SftpDownloadProvider>(builder: (__, pro, _) {
|
||||||
|
if (pro.status.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(s.sftpNoDownloadTask),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(11),
|
||||||
|
itemCount: pro.status.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final status = pro.status[index];
|
||||||
|
return _buildItem(status);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _wrapInCard(SftpDownloadStatus status, String? subtitle,
|
||||||
|
{Widget? trailing}) {
|
||||||
|
return RoundRectCard(ListTile(
|
||||||
|
title: Text(status.fileName),
|
||||||
|
subtitle: subtitle == null
|
||||||
|
? null
|
||||||
|
: Text(
|
||||||
|
subtitle,
|
||||||
|
style: grey,
|
||||||
|
),
|
||||||
|
trailing: trailing,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(SftpDownloadStatus status) {
|
||||||
|
if (status.error != null) {
|
||||||
|
showSnackBar(context, Text(status.error.toString()));
|
||||||
|
}
|
||||||
|
switch (status.status) {
|
||||||
|
case SftpWorkerStatus.finished:
|
||||||
|
final time = status.spentTime.toString();
|
||||||
|
return _wrapInCard(status,
|
||||||
|
'${s.downloadFinished} ${s.spentTime(time == 'null' ? s.unknown : (time.substring(0, time.length - 7)))}',
|
||||||
|
trailing: IconButton(
|
||||||
|
onPressed: () => shareFiles(context, [status.item.localPath]),
|
||||||
|
icon: const Icon(Icons.open_in_new)));
|
||||||
|
case SftpWorkerStatus.downloading:
|
||||||
|
return _wrapInCard(
|
||||||
|
status,
|
||||||
|
s.downloadStatus((status.progress ?? 0.0).toStringAsFixed(2),
|
||||||
|
(status.size ?? 0).convertBytes),
|
||||||
|
trailing:
|
||||||
|
CircularProgressIndicator(value: (status.progress ?? 0) / 100));
|
||||||
|
case SftpWorkerStatus.preparing:
|
||||||
|
return _wrapInCard(status, s.sftpDlPrepare, trailing: loadingIcon);
|
||||||
|
case SftpWorkerStatus.sshConnectted:
|
||||||
|
return _wrapInCard(status, s.sftpSSHConnected, trailing: loadingIcon);
|
||||||
|
default:
|
||||||
|
return _wrapInCard(status, s.unknown,
|
||||||
|
trailing: const Icon(
|
||||||
|
Icons.error,
|
||||||
|
size: 40,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
431
lib/view/page/sftp/view.dart
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:toolbox/core/extension/numx.dart';
|
||||||
|
import 'package:toolbox/core/extension/stringx.dart';
|
||||||
|
import 'package:toolbox/core/route.dart';
|
||||||
|
import 'package:toolbox/core/utils.dart';
|
||||||
|
import 'package:toolbox/data/model/server/server.dart';
|
||||||
|
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:toolbox/data/model/sftp/absolute_path.dart';
|
||||||
|
import 'package:toolbox/data/model/sftp/download_worker.dart';
|
||||||
|
import 'package:toolbox/data/model/sftp/browser_status.dart';
|
||||||
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
|
import 'package:toolbox/data/provider/sftp_download.dart';
|
||||||
|
import 'package:toolbox/data/res/path.dart';
|
||||||
|
import 'package:toolbox/data/store/private_key.dart';
|
||||||
|
import 'package:toolbox/generated/l10n.dart';
|
||||||
|
import 'package:toolbox/locator.dart';
|
||||||
|
import 'package:toolbox/view/page/sftp/downloading.dart';
|
||||||
|
import 'package:toolbox/view/widget/fade_in.dart';
|
||||||
|
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
|
class SFTPPage extends StatefulWidget {
|
||||||
|
final ServerPrivateInfo spi;
|
||||||
|
const SFTPPage(this.spi, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SFTPPageState createState() => _SFTPPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SFTPPageState extends State<SFTPPage> {
|
||||||
|
final SftpBrowserStatus _status = SftpBrowserStatus();
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
late MediaQueryData _media;
|
||||||
|
late S s;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_media = MediaQuery.of(context);
|
||||||
|
s = S.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_status.spi = widget.spi;
|
||||||
|
_status.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: (() => showRoundDialog(
|
||||||
|
context,
|
||||||
|
s.choose,
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.folder),
|
||||||
|
title: Text(s.createFolder),
|
||||||
|
onTap: () => mkdir(context)),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.insert_drive_file),
|
||||||
|
title: Text(s.createFile),
|
||||||
|
onTap: () => newFile(context)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.close))
|
||||||
|
])),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
body: _buildFileView(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get centerCircleLoading => Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: _media.size.height * 0.4,
|
||||||
|
),
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildFileView() {
|
||||||
|
if (!_status.selected) {
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
_buildDestSelector(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final spi = _status.spi;
|
||||||
|
final si =
|
||||||
|
locator<ServerProvider>().servers.firstWhere((s) => s.info == spi);
|
||||||
|
final client = si.client;
|
||||||
|
if (client == null ||
|
||||||
|
si.connectionState != ServerConnectionState.connected) {
|
||||||
|
return centerCircleLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_status.files == null) {
|
||||||
|
_status.path = AbsolutePath('/');
|
||||||
|
listDir(path: '/', client: client);
|
||||||
|
return centerCircleLoading;
|
||||||
|
} else {
|
||||||
|
return RefreshIndicator(
|
||||||
|
child: FadeIn(
|
||||||
|
key: Key(_status.spi!.name + _status.path!.path),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _status.files!.length + 1,
|
||||||
|
controller: _scrollController,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return _buildDestSelector();
|
||||||
|
}
|
||||||
|
final file = _status.files![index - 1];
|
||||||
|
final isDir = file.attr.isDirectory;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
|
||||||
|
title: Text(file.filename),
|
||||||
|
trailing: Text(
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
(file.attr.modifyTime ?? 0) * 1000)
|
||||||
|
.toString()
|
||||||
|
.replaceFirst('.000', ''),
|
||||||
|
style: const TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
subtitle:
|
||||||
|
isDir ? null : Text((file.attr.size ?? 0).convertBytes),
|
||||||
|
onTap: () {
|
||||||
|
if (isDir) {
|
||||||
|
_status.path?.update(file.filename);
|
||||||
|
listDir(path: _status.path?.path);
|
||||||
|
} else {
|
||||||
|
onItemPress(context, file, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () => onItemPress(context, file, false),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onRefresh: () => listDir(path: _status.path?.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onItemPress(BuildContext context, SftpName file, bool showDownload) {
|
||||||
|
showRoundDialog(
|
||||||
|
context,
|
||||||
|
s.choose,
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete),
|
||||||
|
title: Text(s.delete),
|
||||||
|
onTap: () => delete(context, file),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.edit),
|
||||||
|
title: Text(s.rename),
|
||||||
|
onTap: () => rename(context, file),
|
||||||
|
),
|
||||||
|
showDownload
|
||||||
|
? ListTile(
|
||||||
|
leading: const Icon(Icons.download),
|
||||||
|
title: Text(s.download),
|
||||||
|
onTap: () => download(context, file),
|
||||||
|
)
|
||||||
|
: const SizedBox()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.cancel))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void download(BuildContext context, SftpName name) {
|
||||||
|
showRoundDialog(context, s.download,
|
||||||
|
Text('${s.dl2Local(name.filename)}\n${s.keepForeground}'), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final prePath = _status.path!.path;
|
||||||
|
final remotePath =
|
||||||
|
prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
|
||||||
|
final local = '${(await sftpDownloadDir).path}$remotePath';
|
||||||
|
final pubKeyId = _status.spi!.pubKeyId;
|
||||||
|
locator<SftpDownloadProvider>().add(
|
||||||
|
DownloadItem(_status.spi!, remotePath, local),
|
||||||
|
key: pubKeyId == null
|
||||||
|
? null
|
||||||
|
: locator<PrivateKeyStore>().get(pubKeyId).privateKey);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showRoundDialog(context, s.goSftpDlPage, const SizedBox(), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
|
||||||
|
.go(context);
|
||||||
|
},
|
||||||
|
child: Text(s.ok))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
child: Text(s.download))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete(BuildContext context, SftpName file) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showRoundDialog(context, s.attention, Text(s.sureDelete(file.filename)), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_status.client!.remove(file.filename);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
listDir();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
s.delete,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mkdir(BuildContext context) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final textController = TextEditingController();
|
||||||
|
showRoundDialog(
|
||||||
|
context,
|
||||||
|
s.createFolder,
|
||||||
|
TextField(
|
||||||
|
controller: textController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: s.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (textController.text == '') {
|
||||||
|
showRoundDialog(
|
||||||
|
context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.ok)),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_status.client!
|
||||||
|
.mkdir('${_status.path!.path}/${textController.text}');
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
listDir();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
s.ok,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void newFile(BuildContext context) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final textController = TextEditingController();
|
||||||
|
showRoundDialog(
|
||||||
|
context,
|
||||||
|
s.createFile,
|
||||||
|
TextField(
|
||||||
|
controller: textController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: s.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (textController.text == '') {
|
||||||
|
showRoundDialog(
|
||||||
|
context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.ok)),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(await _status.client!
|
||||||
|
.open('${_status.path!.path}/${textController.text}'))
|
||||||
|
.writeBytes(Uint8List(0));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
listDir();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
s.ok,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rename(BuildContext context, SftpName file) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final textController = TextEditingController();
|
||||||
|
showRoundDialog(
|
||||||
|
context,
|
||||||
|
s.rename,
|
||||||
|
TextField(
|
||||||
|
controller: textController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: s.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (textController.text == '') {
|
||||||
|
showRoundDialog(
|
||||||
|
context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(s.ok)),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _status.client!
|
||||||
|
.rename(file.filename, textController.text);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
listDir();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
s.rename,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> listDir({String? path, SSHClient? client}) async {
|
||||||
|
if (_status.isBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_status.isBusy = true;
|
||||||
|
if (client != null) {
|
||||||
|
final sftpc = await client.sftp();
|
||||||
|
_status.client = sftpc;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final fs =
|
||||||
|
await _status.client!.listdir(path ?? (_status.path?.path ?? '/'));
|
||||||
|
fs.sort((a, b) => a.filename.compareTo(b.filename));
|
||||||
|
fs.removeAt(0);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_status.files = fs;
|
||||||
|
_status.isBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await showRoundDialog(context, s.error, Text(e.toString()), [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
|
||||||
|
]);
|
||||||
|
if (_status.path!.undo()) {
|
||||||
|
await listDir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDestSelector() {
|
||||||
|
final str = _status.path?.path;
|
||||||
|
return ExpansionTile(
|
||||||
|
title: Text(_status.spi?.name ?? s.chooseDestination),
|
||||||
|
subtitle: _status.selected
|
||||||
|
? str!.omitStartStr(style: const TextStyle(color: Colors.grey))
|
||||||
|
: null,
|
||||||
|
children: locator<ServerProvider>()
|
||||||
|
.servers
|
||||||
|
.map((e) => _buildDestSelectorItem(e.info))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDestSelectorItem(ServerPrivateInfo spi) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(spi.name),
|
||||||
|
subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'),
|
||||||
|
onTap: () {
|
||||||
|
_status.spi = spi;
|
||||||
|
_status.selected = true;
|
||||||
|
_status.path = AbsolutePath('/');
|
||||||
|
listDir(
|
||||||
|
client: locator<ServerProvider>()
|
||||||
|
.servers
|
||||||
|
.firstWhere((s) => s.info == spi)
|
||||||
|
.client,
|
||||||
|
path: '/');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||