Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
86a700d0bb |
1
.gitignore
vendored
@@ -47,3 +47,4 @@ app.*.map.json
|
||||
|
||||
/android/app/fjy.androidstudio.key
|
||||
/release
|
||||
test.dart
|
||||
|
||||
51
README.md
@@ -1,31 +1,48 @@
|
||||
# Server Monitor & Toolbox
|
||||
A new Flutter project which provide a chart view to display server status data and a manager toolbox.
|
||||
|
||||
A new Flutter project which provide a chart view to display server status data.
|
||||
## Milestone
|
||||
- [x] Status chart view
|
||||
- [x] Base64/Url En/Decode
|
||||
- [x] Ping
|
||||
- [x] Desktop support
|
||||
- [x] Apt/Yum manager
|
||||
- [x] SFTP
|
||||
- [ ] Snippet market
|
||||
- [x] Docker manager
|
||||
- [x] i18n (English, Chinese)
|
||||
|
||||
## ScreenShots
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3327.PNG">
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3347.PNG">
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_detail.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/detail.jpg">
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/server_edit.jpg">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3330.PNG">
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/convert.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3331.PNG">
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/ping.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/IMG_3346.PNG">
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/setting.jpg">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img width="200px" src="https://raw.githubusercontent.com/LollipopKit/flutter_server_monitor_toolbox/main/screenshots/drawer.jpg">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -36,27 +53,13 @@ Status|Platform
|
||||
Full Support|Android/iOS/macOS
|
||||
Support, but not tested|Windows/Linux
|
||||
|
||||
## Milestone
|
||||
- [x] SSH connect
|
||||
- [x] Server info store
|
||||
- [x] Status chart view
|
||||
- [x] Base64/Url En/Decode
|
||||
- [x] Private key store
|
||||
- [x] Server status detail page
|
||||
- [x] Theme switch
|
||||
- [x] Execute snippet
|
||||
- [x] Migrate from `ssh2` to `dartssh2`
|
||||
- [x] Desktop support
|
||||
|
||||
## Build
|
||||
Please use `make.dart` to build.
|
||||
```shell
|
||||
# build android apk
|
||||
./make.dart build android
|
||||
# due to pub package 'ssh2' incompatibility
|
||||
# can't build ios ipa through './make.dart build ios'
|
||||
# more info: [https://github.com/jda258/flutter_ssh2/issues/8]
|
||||
# please run below cmd to run on ios device
|
||||
# build android apk and ios archive
|
||||
./make.dart build
|
||||
# Run in release mode
|
||||
./make.dart run release
|
||||
```
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ linter:
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
library_private_types_in_public_api: false
|
||||
use_build_context_synchronously: false
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
|
||||
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
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
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
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.6.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
||||
1
apk.sksl.json
Normal file
@@ -1,19 +1,25 @@
|
||||
PODS:
|
||||
- countly_flutter (20.11.4):
|
||||
- countly_flutter (21.11.0):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- r_upgrade (0.0.1):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -21,20 +27,26 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/countly_flutter/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
r_upgrade:
|
||||
:path: ".symlinks/plugins/r_upgrade/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
countly_flutter: 38419412e193a1faa5babeb5d28a63fda260687d
|
||||
countly_flutter: e245f94349d8adf306c22e60c10648c69aae7380
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
|
||||
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 136;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -362,7 +362,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.79;
|
||||
MARKETING_VERSION = 1.0.136;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -484,7 +484,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 136;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -492,7 +492,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.79;
|
||||
MARKETING_VERSION = 1.0.136;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -508,7 +508,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 136;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -516,7 +516,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.79;
|
||||
MARKETING_VERSION = 1.0.136;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
|
||||
@@ -5,11 +5,42 @@
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "darkbackground.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
||||
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"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>ServerBox</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>ServerBox</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>zh</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
1
ipa.sksl.json
Normal file
11
lib/app.dart
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/home.dart';
|
||||
|
||||
@@ -24,12 +27,20 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
setTransparentNavigationBar(context);
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: locator<SettingStore>().primaryColor.listenable(),
|
||||
builder: (_, value, __) {
|
||||
final primaryColor = Color(value);
|
||||
final textStyle = TextStyle(color: primaryColor);
|
||||
return MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
title: BuildData.name,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:countly_flutter/countly_config.dart';
|
||||
import 'package:countly_flutter/countly_flutter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class Analysis {
|
||||
static const _url = 'https://countly.xuty.cc';
|
||||
@@ -9,12 +12,18 @@ class Analysis {
|
||||
static bool _enabled = false;
|
||||
|
||||
static Future<void> init(bool debug) async {
|
||||
_enabled = true;
|
||||
await Countly.setLoggingEnabled(debug);
|
||||
await Countly.init(_url, _key);
|
||||
await Countly.start();
|
||||
await Countly.enableCrashReporting();
|
||||
await Countly.giveAllConsent();
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
_enabled = true;
|
||||
final config = CountlyConfig(_url, _key)
|
||||
.setLoggingEnabled(debug)
|
||||
.enableCrashReporting();
|
||||
await Countly.initWithConfig(config);
|
||||
await Countly.start();
|
||||
await Countly.giveAllConsent();
|
||||
} else {
|
||||
Logger('COUNTLY')
|
||||
.info('Unsupported platform ${Platform.operatingSystem}');
|
||||
}
|
||||
}
|
||||
|
||||
static void recordView(String view) {
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:r_upgrade/r_upgrade.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/data/service/app.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
final logger = Logger('UPDATE');
|
||||
|
||||
Future<bool> isFileAvailable(String url) async {
|
||||
try {
|
||||
final resp = await Dio().head(url);
|
||||
return resp.statusCode == 200;
|
||||
} catch (e) {
|
||||
print('update file not available: $e');
|
||||
logger.warning('update file not available: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -26,22 +28,35 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
||||
|
||||
locator<AppProvider>().setNewestBuild(update.newest);
|
||||
|
||||
if (!force && update.newest <= BuildData.build) {
|
||||
print('Update ignored due to current: ${BuildData.build}, '
|
||||
'update: ${update.newest}');
|
||||
final newest = () {
|
||||
if (Platform.isAndroid) {
|
||||
return update.androidbuild;
|
||||
} else if (Platform.isIOS) {
|
||||
return update.iosbuild;
|
||||
} else if (Platform.isMacOS) {
|
||||
return update.macbuild;
|
||||
}
|
||||
return update.newest;
|
||||
}();
|
||||
|
||||
if (!force && newest <= BuildData.build) {
|
||||
logger.info('Update ignored due to current: ${BuildData.build}, '
|
||||
'update: $newest');
|
||||
return;
|
||||
}
|
||||
print('Update available: ${update.newest}');
|
||||
logger.info('Update available: $newest');
|
||||
|
||||
if (Platform.isAndroid && !await isFileAvailable(update.android)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final s = S.of(context);
|
||||
|
||||
showSnackBarWithAction(
|
||||
context,
|
||||
update.min > BuildData.build
|
||||
? 'Your version is too old. \nPlease update to v1.0.${update.newest}.'
|
||||
: 'Update: v1.0.${update.newest} available. \n${update.changelog}',
|
||||
? 'Your version is too old. \nPlease update to v1.0.$newest.'
|
||||
: 'Update: v1.0.$newest available. \n${update.changelog}',
|
||||
'Update', () async {
|
||||
if (Platform.isAndroid) {
|
||||
await RUpgrade.upgrade(update.android,
|
||||
@@ -51,5 +66,9 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
||||
} else if (Platform.isMacOS) {
|
||||
await RUpgrade.upgradeFromUrl(update.mac);
|
||||
}
|
||||
showRoundDialog(context, s.attention, Text(s.platformNotSupportUpdate), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/view/widget/card_dialog.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
|
||||
void unawaited(Future<void> future) {}
|
||||
|
||||
@@ -27,10 +30,11 @@ void showSnackBarWithAction(BuildContext context, String content, String action,
|
||||
}
|
||||
|
||||
Future<bool> openUrl(String url) async {
|
||||
if (!await canLaunch(url)) {
|
||||
final uri = url.uri;
|
||||
if (!await canLaunchUrl(uri)) {
|
||||
return false;
|
||||
}
|
||||
final ok = await launch(url, forceSafariVC: false);
|
||||
final ok = await launchUrl(uri);
|
||||
if (ok == true) {
|
||||
return true;
|
||||
}
|
||||
@@ -71,12 +75,38 @@ Widget buildSwitch(BuildContext context, StoreProperty<bool> prop,
|
||||
void setTransparentNavigationBar(BuildContext context) {
|
||||
if (Platform.isAndroid) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarContrastEnforced: true,
|
||||
systemNavigationBarIconBrightness:
|
||||
isDarkMode(context) ? Brightness.light : Brightness.dark,
|
||||
));
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarContrastEnforced: true));
|
||||
}
|
||||
}
|
||||
|
||||
String tabTitleName(BuildContext context, int i) {
|
||||
final s = S.of(context);
|
||||
switch (i) {
|
||||
case 0:
|
||||
return s.server;
|
||||
case 1:
|
||||
return s.convert;
|
||||
case 2:
|
||||
return s.ping;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> shareFiles(BuildContext context, List<String> filePaths) async {
|
||||
for (final filePath in filePaths) {
|
||||
if (!await File(filePath).exists()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var text = '';
|
||||
if (filePaths.length == 1) {
|
||||
text = filePaths.first.split('/').last;
|
||||
} else {
|
||||
text = '${filePaths.length} ${S.of(context).files}';
|
||||
}
|
||||
await Share.shareFiles(filePaths, text: 'ServerBox -> $text');
|
||||
return filePaths.isNotEmpty;
|
||||
}
|
||||
|
||||
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) {
|
||||
newest = json["newest"]?.toInt();
|
||||
newest = json["macbuild"]?.toInt();
|
||||
newest = json["iosbuild"]?.toInt();
|
||||
newest = json["androidbuild"]?.toInt();
|
||||
macbuild = json["macbuild"]?.toInt();
|
||||
iosbuild = json["iosbuild"]?.toInt();
|
||||
androidbuild = json["androidbuild"]?.toInt();
|
||||
android = json["android"].toString();
|
||||
ios = json["ios"].toString();
|
||||
min = json["min"].toInt();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Cpu2Status update(List<CpuStatus> newStatus, String newTemp) {
|
||||
return Cpu2Status(now, newStatus, newTemp);
|
||||
void update(List<CpuStatus> newStatus, String newTemp) {
|
||||
pre = now;
|
||||
now = newStatus;
|
||||
temp = newTemp;
|
||||
}
|
||||
|
||||
int get coresCount => now.length;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'dart:math';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
|
||||
class NetSpeedPart {
|
||||
String device;
|
||||
@@ -21,51 +21,40 @@ class NetSpeed {
|
||||
return devices;
|
||||
}
|
||||
|
||||
NetSpeed update(List<NetSpeedPart> newOne) => NetSpeed(now, newOne);
|
||||
void update(List<NetSpeedPart> newOne) {
|
||||
old = now;
|
||||
now = newOne;
|
||||
}
|
||||
|
||||
int get timeDiff => now[0].time - old[0].time;
|
||||
|
||||
String speedIn({String? device}) {
|
||||
if (old[0].device == '' || now[0].device == '') return '0kb/s';
|
||||
int idx = 0;
|
||||
if (device != null) {
|
||||
for (var item in now) {
|
||||
if (item.device == device) {
|
||||
idx = now.indexOf(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final idx = deviceIdx(device);
|
||||
final speedInBytesPerSecond =
|
||||
(now[idx].bytesIn - old[idx].bytesIn) / timeDiff;
|
||||
int squareTimes = 0;
|
||||
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
|
||||
if (squareTimes >= suffixs.length - 1) break;
|
||||
squareTimes++;
|
||||
}
|
||||
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
|
||||
return buildStandardOutput(speedInBytesPerSecond);
|
||||
}
|
||||
|
||||
String speedOut({String? device}) {
|
||||
if (old[0].device == '' || now[0].device == '') return '0kb/s';
|
||||
int idx = 0;
|
||||
final idx = deviceIdx(device);
|
||||
final speedInBytesPerSecond =
|
||||
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
|
||||
return buildStandardOutput(speedInBytesPerSecond);
|
||||
}
|
||||
|
||||
int deviceIdx(String? device) {
|
||||
if (device != null) {
|
||||
for (var item in now) {
|
||||
if (item.device == device) {
|
||||
idx = now.indexOf(item);
|
||||
break;
|
||||
return now.indexOf(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
final speedInBytesPerSecond =
|
||||
(now[idx].bytesOut - old[idx].bytesOut) / timeDiff;
|
||||
int squareTimes = 0;
|
||||
for (; speedInBytesPerSecond / pow(1024, squareTimes) > 1024;) {
|
||||
if (squareTimes >= suffixs.length - 1) break;
|
||||
squareTimes++;
|
||||
}
|
||||
return '${(speedInBytesPerSecond / pow(1024, squareTimes)).toStringAsFixed(1)} ${suffixs[squareTimes]}';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const suffixs = ['b/s', 'kb/s', 'mb/s', 'gb/s'];
|
||||
String buildStandardOutput(double speed) =>
|
||||
'${speed.convertBytes.toLowerCase()}/s';
|
||||
}
|
||||
|
||||
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)!);
|
||||
}
|
||||
}
|
||||
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 }
|
||||
111
lib/data/model/sftp/download_worker.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:easy_isolate/easy_isolate.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/sftp/download_status.dart';
|
||||
|
||||
class DownloadItem {
|
||||
DownloadItem(this.spi, this.remotePath, this.localPath);
|
||||
|
||||
final ServerPrivateInfo spi;
|
||||
final String remotePath;
|
||||
final String localPath;
|
||||
}
|
||||
|
||||
class SftpDownloadWorker {
|
||||
SftpDownloadWorker(
|
||||
{required this.onNotify, required this.item, this.privateKey});
|
||||
|
||||
final Function(Object event) onNotify;
|
||||
final DownloadItem item;
|
||||
final worker = Worker();
|
||||
final String? privateKey;
|
||||
|
||||
void dispose() {
|
||||
worker.dispose();
|
||||
}
|
||||
|
||||
/// Initiate the worker (new thread) and start listen from messages between
|
||||
/// the threads
|
||||
Future<void> init() async {
|
||||
if (worker.isInitialized) worker.dispose();
|
||||
await worker.init(
|
||||
mainMessageHandler,
|
||||
isolateMessageHandler,
|
||||
errorHandler: print,
|
||||
);
|
||||
worker.sendMessage(DownloadItemEvent(item, privateKey));
|
||||
}
|
||||
|
||||
/// Handle the messages coming from the isolate
|
||||
void mainMessageHandler(dynamic data, SendPort isolateSendPort) {
|
||||
onNotify(data);
|
||||
}
|
||||
|
||||
/// Handle the messages coming from the main
|
||||
static isolateMessageHandler(
|
||||
dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async {
|
||||
if (data is DownloadItemEvent) {
|
||||
try {
|
||||
mainSendPort.send(SftpWorkerStatus.preparing);
|
||||
final watch = Stopwatch()..start();
|
||||
final item = data.item;
|
||||
final spi = item.spi;
|
||||
final socket = await SSHSocket.connect(spi.ip, spi.port);
|
||||
SSHClient client;
|
||||
if (spi.pubKeyId == null) {
|
||||
client = SSHClient(socket,
|
||||
username: spi.user,
|
||||
onPasswordRequest: () => spi.authorization as String);
|
||||
} else {
|
||||
client = SSHClient(socket,
|
||||
username: spi.user,
|
||||
identities: SSHKeyPair.fromPem(data.privateKey!));
|
||||
}
|
||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
||||
|
||||
final remotePath = item.remotePath;
|
||||
final localPath = item.localPath;
|
||||
await Directory(localPath.substring(0, item.localPath.lastIndexOf('/')))
|
||||
.create(recursive: true);
|
||||
final local = File(localPath);
|
||||
if (await local.exists()) {
|
||||
await local.delete();
|
||||
}
|
||||
final localFile = local.openWrite(mode: FileMode.append);
|
||||
final file = await (await client.sftp()).open(remotePath);
|
||||
final size = (await file.stat()).size;
|
||||
if (size == null) {
|
||||
mainSendPort.send(Exception('can not get file size'));
|
||||
return;
|
||||
}
|
||||
const defaultChunkSize = 1024 * 1024;
|
||||
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
|
||||
mainSendPort.send(size);
|
||||
mainSendPort.send(SftpWorkerStatus.downloading);
|
||||
for (var i = 0; i < size; i += chunkSize) {
|
||||
final fileData = file.read(length: chunkSize);
|
||||
await for (var form in fileData) {
|
||||
localFile.add(form);
|
||||
mainSendPort.send((i + form.length) / size * 100);
|
||||
}
|
||||
}
|
||||
localFile.close();
|
||||
mainSendPort.send(watch.elapsed);
|
||||
mainSendPort.send(SftpWorkerStatus.finished);
|
||||
} catch (e) {
|
||||
mainSendPort.send(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadItemEvent {
|
||||
DownloadItemEvent(this.item, this.privateKey);
|
||||
|
||||
final DownloadItem item;
|
||||
final String? privateKey;
|
||||
}
|
||||
164
lib/data/provider/apt.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
|
||||
import 'package:toolbox/data/model/distribution.dart';
|
||||
|
||||
typedef PwdRequestFunc = Future<String> Function(
|
||||
bool lastTimes, String? userName);
|
||||
final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):');
|
||||
|
||||
class AptProvider extends BusyProvider {
|
||||
final logger = Logger('AptProvider');
|
||||
|
||||
SSHClient? client;
|
||||
Distribution? dist;
|
||||
Function()? onUpgrade;
|
||||
Function()? onUpdate;
|
||||
PwdRequestFunc? onPasswordRequest;
|
||||
|
||||
String? whoami;
|
||||
List<UpgradePkgInfo>? upgradeable;
|
||||
String? error;
|
||||
String? upgradeLog;
|
||||
String? updateLog;
|
||||
String lastLog = '';
|
||||
int triedTimes = 0;
|
||||
|
||||
AptProvider();
|
||||
|
||||
Future<void> init(SSHClient client, Distribution dist, Function() onUpgrade,
|
||||
Function() onUpdate, PwdRequestFunc onPasswordRequest) async {
|
||||
this.client = client;
|
||||
this.dist = dist;
|
||||
this.onUpgrade = onUpgrade;
|
||||
this.onPasswordRequest = onPasswordRequest;
|
||||
whoami = (await client.run('whoami').string).trim();
|
||||
}
|
||||
|
||||
bool get isSU => whoami == 'root';
|
||||
|
||||
void clear() {
|
||||
client = null;
|
||||
dist = null;
|
||||
upgradeable = null;
|
||||
error = null;
|
||||
upgradeLog = null;
|
||||
updateLog = whoami = null;
|
||||
onUpgrade = null;
|
||||
onUpdate = null;
|
||||
onPasswordRequest = null;
|
||||
triedTimes = 0;
|
||||
}
|
||||
|
||||
Future<void> refreshInstalled() async {
|
||||
if (client == null) {
|
||||
error = 'No client';
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _update();
|
||||
try {
|
||||
getUpgradeableList(result);
|
||||
} catch (e) {
|
||||
error = '[Server Raw]:\n$result\n[App Error]:\n$e';
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void getUpgradeableList(String? raw) {
|
||||
if (raw == null) return;
|
||||
var list = raw.split('\n');
|
||||
switch (dist) {
|
||||
case Distribution.rehl:
|
||||
list = list.sublist(2);
|
||||
list.removeWhere((element) => element.isEmpty);
|
||||
final endLine = list.lastIndexWhere(
|
||||
(element) => element.contains('Obsoleting Packages'));
|
||||
list = list.sublist(0, endLine);
|
||||
break;
|
||||
default:
|
||||
list = list.sublist(4);
|
||||
list.removeWhere((element) => element.isEmpty);
|
||||
}
|
||||
upgradeable = list.map((e) => UpgradePkgInfo(e, dist!)).toList();
|
||||
}
|
||||
|
||||
Future<String> _update() async {
|
||||
switch (dist) {
|
||||
case Distribution.rehl:
|
||||
return await client?.run(_wrap('yum check-update')).string ?? '';
|
||||
default:
|
||||
final session = await client!.execute(_wrap('apt update'));
|
||||
session.stderr.listen((event) => _onPwd(event, session.stdin));
|
||||
session.stdout.listen((event) {
|
||||
updateLog = (updateLog ?? '') + event.string;
|
||||
notifyListeners();
|
||||
onUpdate ?? () {}();
|
||||
});
|
||||
await session.done;
|
||||
return await client
|
||||
?.run('apt list --upgradeable'.withLangExport)
|
||||
.string ??
|
||||
'';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> upgrade() async {
|
||||
if (client == null) {
|
||||
error = 'No client';
|
||||
return;
|
||||
}
|
||||
|
||||
final upgradeCmd = () {
|
||||
switch (dist) {
|
||||
case Distribution.rehl:
|
||||
return 'yum upgrade -y';
|
||||
default:
|
||||
return 'apt upgrade -y';
|
||||
}
|
||||
}();
|
||||
|
||||
final session = await client!.execute(_wrap(upgradeCmd));
|
||||
session.stderr.listen((e) => _onPwd(e, session.stdin));
|
||||
|
||||
session.stdout.listen((data) async {
|
||||
final log = data.string;
|
||||
if (lastLog == log.trim()) return;
|
||||
upgradeLog = (upgradeLog ?? '') + log;
|
||||
lastLog = log.trim();
|
||||
notifyListeners();
|
||||
onUpgrade!();
|
||||
});
|
||||
|
||||
upgradeLog = null;
|
||||
await session.done;
|
||||
refreshInstalled();
|
||||
}
|
||||
|
||||
Future<void> _onPwd(Uint8List e, StreamSink<Uint8List> stdin) async {
|
||||
final event = e.string;
|
||||
if (event.contains('[sudo] password for ')) {
|
||||
final user = pwdRequestWithUserReg.firstMatch(event)?.group(1);
|
||||
logger.info('sudo password request for $user');
|
||||
triedTimes++;
|
||||
final pwd =
|
||||
await (onPasswordRequest ?? (_) async => '')(triedTimes == 3, user);
|
||||
if (pwd.isEmpty) {
|
||||
logger.info('sudo password request cancelled');
|
||||
return;
|
||||
}
|
||||
stdin.add(Uint8List.fromList(utf8.encode('$pwd\n')));
|
||||
}
|
||||
}
|
||||
|
||||
String _wrap(String cmd) =>
|
||||
'export LANG=en_US.utf-8 && ${isSU ? "" : "sudo -S "}$cmd';
|
||||
}
|
||||
@@ -34,8 +34,8 @@ class DebugProvider extends ChangeNotifier {
|
||||
),
|
||||
);
|
||||
_addWidget(SingleChildScrollView(
|
||||
child: widget,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: widget,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
107
lib/data/provider/docker.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:toolbox/data/model/docker/ps.dart';
|
||||
|
||||
final dockerNotFound = RegExp(r'command not found|Unknown command');
|
||||
|
||||
class DockerProvider extends BusyProvider {
|
||||
SSHClient? client;
|
||||
List<DockerPsItem>? items;
|
||||
String? version;
|
||||
String? edition;
|
||||
String? error;
|
||||
|
||||
void init(SSHClient client) => this.client = client;
|
||||
|
||||
void clear() {
|
||||
client = null;
|
||||
error = null;
|
||||
items = null;
|
||||
version = null;
|
||||
edition = null;
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (client == null) {
|
||||
error = 'no client';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final verRaw = await client!.run('docker version'.withLangExport).string;
|
||||
if (verRaw.contains(dockerNotFound)) {
|
||||
error = 'docker not found';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
final verSplit = verRaw.split('\n');
|
||||
if (verSplit.length < 3) {
|
||||
error = 'invalid version';
|
||||
notifyListeners();
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
version = verSplit[1].split(' ').last;
|
||||
edition = verSplit[0].split(': ')[1];
|
||||
} catch (e) {
|
||||
error = e.toString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final raw = await client!.run('docker ps -a'.withLangExport).string;
|
||||
final lines = raw.split('\n');
|
||||
lines.removeAt(0);
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
|
||||
try {
|
||||
items = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
|
||||
} catch (e) {
|
||||
error = e.toString();
|
||||
rethrow;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> stop(String id) async {
|
||||
setBusyState();
|
||||
if (client == null) {
|
||||
error = 'no client';
|
||||
setBusyState(false);
|
||||
return false;
|
||||
}
|
||||
final result = await client!.run('docker stop $id').string;
|
||||
await refresh();
|
||||
setBusyState(false);
|
||||
return result.contains(id);
|
||||
}
|
||||
|
||||
Future<bool> start(String id) async {
|
||||
setBusyState();
|
||||
if (client == null) {
|
||||
error = 'no client';
|
||||
setBusyState(false);
|
||||
return false;
|
||||
}
|
||||
final result = await client!.run('docker start $id').string;
|
||||
await refresh();
|
||||
setBusyState(false);
|
||||
return result.contains(id);
|
||||
}
|
||||
|
||||
Future<bool> delete(String id) async {
|
||||
setBusyState();
|
||||
if (client == null) {
|
||||
error = 'no client';
|
||||
setBusyState(false);
|
||||
return false;
|
||||
}
|
||||
final result = await client!.run('docker rm $id').string;
|
||||
await refresh();
|
||||
setBusyState(false);
|
||||
return result.contains(id);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:toolbox/data/model/server/cpu_2_status.dart';
|
||||
import 'package:toolbox/data/model/server/cpu_status.dart';
|
||||
@@ -26,13 +26,24 @@ import 'package:toolbox/locator.dart';
|
||||
/// Because of this function is called by [compute] in [ServerProvider.genClient].
|
||||
/// https://stackoverflow.com/questions/51998995/invalid-arguments-illegal-argument-in-isolate-message-object-is-a-closure
|
||||
List<SSHKeyPair> loadIndentity(String key) {
|
||||
final watch = Stopwatch()..start();
|
||||
final pem = SSHKeyPair.fromPem(key);
|
||||
watch.stop();
|
||||
print('loadIndentity: ${watch.elapsedMilliseconds}ms');
|
||||
return pem;
|
||||
return SSHKeyPair.fromPem(key);
|
||||
}
|
||||
|
||||
const seperator = 'A====A';
|
||||
const shellCmd = "export LANG=en_US.utf-8 \necho '$seperator' \n"
|
||||
"cat /proc/net/dev && date +%s \necho $seperator \n "
|
||||
"cat /etc/os-release | grep PRETTY_NAME \necho $seperator \n"
|
||||
"cat /proc/stat | grep cpu \necho $seperator \n"
|
||||
"uptime \necho $seperator \n"
|
||||
"cat /proc/net/snmp \necho $seperator \n"
|
||||
"df -h \necho $seperator \n"
|
||||
"free -m \necho $seperator \n"
|
||||
"cat /sys/class/thermal/thermal_zone*/type \necho $seperator \n"
|
||||
"cat /sys/class/thermal/thermal_zone*/temp";
|
||||
const shellPath = '.serverbox.sh';
|
||||
const memPrefix = 'Mem:';
|
||||
final cpuTempReg = RegExp('(x86_pkg_temp|cpu_thermal)');
|
||||
|
||||
class ServerProvider extends BusyProvider {
|
||||
List<ServerInfo> _servers = [];
|
||||
List<ServerInfo> get servers => _servers;
|
||||
@@ -89,22 +100,14 @@ class ServerProvider extends BusyProvider {
|
||||
identities: await compute(loadIndentity, key.privateKey));
|
||||
}
|
||||
|
||||
Future<void> refreshData({int? idx}) async {
|
||||
if (idx != null) {
|
||||
_getData(idx);
|
||||
Future<void> refreshData({ServerPrivateInfo? spi}) async {
|
||||
if (spi != null) {
|
||||
_getData(spi);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Future.wait(_servers.map((s) async {
|
||||
final idx = _servers.indexOf(s);
|
||||
if (idx == -1) return;
|
||||
await _getData(idx);
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e is! RangeError) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
await Future.wait(_servers.map((s) async {
|
||||
await _getData(s.info);
|
||||
}));
|
||||
}
|
||||
|
||||
Future<void> startAutoRefresh() async {
|
||||
@@ -124,15 +127,22 @@ class ServerProvider extends BusyProvider {
|
||||
}
|
||||
}
|
||||
|
||||
void addServer(ServerPrivateInfo info) {
|
||||
_servers.add(genInfo(info));
|
||||
locator<ServerStore>().put(info);
|
||||
void setDisconnected() {
|
||||
for (var i = 0; i < _servers.length; i++) {
|
||||
_servers[i].connectionState = ServerConnectionState.disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
void addServer(ServerPrivateInfo spi) {
|
||||
_servers.add(genInfo(spi));
|
||||
locator<ServerStore>().put(spi);
|
||||
notifyListeners();
|
||||
refreshData(idx: _servers.length - 1);
|
||||
refreshData(spi: spi);
|
||||
}
|
||||
|
||||
void delServer(ServerPrivateInfo info) {
|
||||
final idx = _servers.indexWhere((s) => s.info == info);
|
||||
if (idx == -1) return;
|
||||
_servers[idx].client?.close();
|
||||
_servers.removeAt(idx);
|
||||
notifyListeners();
|
||||
@@ -140,54 +150,67 @@ class ServerProvider extends BusyProvider {
|
||||
}
|
||||
|
||||
Future<void> updateServer(
|
||||
ServerPrivateInfo old, ServerPrivateInfo newInfo) async {
|
||||
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
|
||||
final idx = _servers.indexWhere((e) => e.info == old);
|
||||
_servers[idx].info = newInfo;
|
||||
_servers[idx].client = await genClient(newInfo);
|
||||
locator<ServerStore>().update(old, newInfo);
|
||||
if (idx < 0) {
|
||||
throw RangeError.index(idx, _servers);
|
||||
}
|
||||
_servers[idx].info = newSpi;
|
||||
_servers[idx].client = await genClient(newSpi);
|
||||
locator<ServerStore>().update(old, newSpi);
|
||||
notifyListeners();
|
||||
refreshData(idx: idx);
|
||||
refreshData(spi: newSpi);
|
||||
}
|
||||
|
||||
Future<void> _getData(int idx) async {
|
||||
final client = _servers[idx].client;
|
||||
final info = _servers[idx].info;
|
||||
final connected = client != null;
|
||||
Future<void> _getData(ServerPrivateInfo spi) async {
|
||||
final idx = _servers.indexWhere((element) => element.info == spi);
|
||||
final state = _servers[idx].connectionState;
|
||||
if (!connected ||
|
||||
if (_servers[idx].client == null ||
|
||||
state == ServerConnectionState.failed ||
|
||||
state == ServerConnectionState.disconnected) {
|
||||
_servers[idx].connectionState = ServerConnectionState.connecting;
|
||||
notifyListeners();
|
||||
final time1 = DateTime.now();
|
||||
try {
|
||||
_servers[idx].client = await genClient(info);
|
||||
_servers[idx].client = await genClient(spi);
|
||||
final time2 = DateTime.now();
|
||||
logger.info(
|
||||
'Connected to [${info.name}] in [${time2.difference(time1).toString()}].');
|
||||
'Connected to [${spi.name}] in [${time2.difference(time1).toString()}].');
|
||||
_servers[idx].connectionState = ServerConnectionState.connected;
|
||||
notifyListeners();
|
||||
_servers[idx]
|
||||
.client!
|
||||
.run("echo '$shellCmd' > $shellPath && chmod +x $shellPath");
|
||||
} catch (e) {
|
||||
_servers[idx].connectionState = ServerConnectionState.failed;
|
||||
_servers[idx].status.failedInfo = e.toString();
|
||||
notifyListeners();
|
||||
_servers[idx].status.failedInfo = '$e ## ';
|
||||
logger.warning(e);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// if client is null, return
|
||||
final si = _servers[idx];
|
||||
if (si.client == null) return;
|
||||
final raw = await si.client!.run("sh $shellPath").string;
|
||||
final lines = raw.split(seperator).map((e) => e.trim()).toList();
|
||||
lines.removeAt(0);
|
||||
|
||||
try {
|
||||
if (_servers[idx].client == null) return;
|
||||
_getCPU(_servers[idx]);
|
||||
_getMem(_servers[idx]);
|
||||
_getSysVer(_servers[idx]);
|
||||
_getUpTime(_servers[idx]);
|
||||
_getDisk(_servers[idx]);
|
||||
_getTcp(_servers[idx]);
|
||||
_getNetSpeed(_servers[idx]);
|
||||
_getCPU(spi, lines[2], lines[7], lines[8]);
|
||||
_getMem(spi, lines[6]);
|
||||
_getSysVer(spi, lines[1]);
|
||||
_getUpTime(spi, lines[3]);
|
||||
_getDisk(spi, lines[5]);
|
||||
_getTcp(spi, lines[4]);
|
||||
_getNetSpeed(spi, lines[0]);
|
||||
} catch (e) {
|
||||
_servers[idx].connectionState = ServerConnectionState.failed;
|
||||
servers[idx].status.failedInfo = e.toString();
|
||||
notifyListeners();
|
||||
logger.warning(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,14 +220,12 @@ class ServerProvider extends BusyProvider {
|
||||
/// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0
|
||||
/// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0
|
||||
/// 1635752901
|
||||
Future<void> _getNetSpeed(ServerInfo info) async {
|
||||
final idx = _servers.indexOf(info);
|
||||
final client = _servers[idx].client!;
|
||||
final raw = utf8.decode(await client.run('cat /proc/net/dev && date +%s'));
|
||||
void _getNetSpeed(ServerPrivateInfo spi, String raw) {
|
||||
final info = _servers.firstWhere((e) => e.info == spi);
|
||||
final split = raw.split('\n');
|
||||
final deviceCount = split.length - 3;
|
||||
if (deviceCount < 1) return;
|
||||
final time = int.parse(split[split.length - 2]);
|
||||
final time = int.parse(split[split.length - 1]);
|
||||
final results = <NetSpeedPart>[];
|
||||
for (int idx = 2; idx < deviceCount; idx++) {
|
||||
final data = split[idx].trim().split(':');
|
||||
@@ -215,41 +236,40 @@ class ServerProvider extends BusyProvider {
|
||||
final bytesOut = int.parse(bytes[8]);
|
||||
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
||||
}
|
||||
info.status.netSpeed = info.status.netSpeed.update(results);
|
||||
notifyListeners();
|
||||
info.status.netSpeed.update(results);
|
||||
}
|
||||
|
||||
Future<void> _getSysVer(ServerInfo info) async {
|
||||
final idx = _servers.indexOf(info);
|
||||
final client = _servers[idx].client!;
|
||||
final raw =
|
||||
utf8.decode(await client.run('cat /etc/os-release | grep PRETTY_NAME'));
|
||||
void _getSysVer(ServerPrivateInfo spi, String raw) {
|
||||
final info = _servers.firstWhere((e) => e.info == spi);
|
||||
final s = raw.split('=');
|
||||
if (s.length == 2) {
|
||||
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
|
||||
} else {
|
||||
info.status.sysVer = '';
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _getCPUTemp(String raw) {
|
||||
final split = raw.split('\n');
|
||||
String _getCPUTemp(String type, String value) {
|
||||
const noMatch = "/sys/class/thermal/thermal_zone*/type";
|
||||
// Not support to get CPU temperature
|
||||
if (value.contains(noMatch) ||
|
||||
type.contains(noMatch) ||
|
||||
value.isEmpty ||
|
||||
type.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
final split = type.split('\n');
|
||||
int idx = 0;
|
||||
for (var item in split) {
|
||||
if (item.contains('x86_pkg_temp') || item.contains('cpu_thermal')) {
|
||||
return item.split(' ').last;
|
||||
if (item.contains(cpuTempReg)) {
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
return '';
|
||||
return '${(int.parse(value.split('\n')[idx].trim()) / 1000).toStringAsFixed(1)}°C';
|
||||
}
|
||||
|
||||
Future<void> _getCPU(ServerInfo info) async {
|
||||
final idx = _servers.indexOf(info);
|
||||
final client = _servers[idx].client!;
|
||||
final raw = utf8.decode(await client.run("cat /proc/stat | grep cpu"));
|
||||
final temp = utf8.decode(await client.run(
|
||||
r"paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/'"));
|
||||
void _getCPU(
|
||||
ServerPrivateInfo spi, String raw, String tempType, String tempValue) {
|
||||
final info = _servers.firstWhere((e) => e.info == spi);
|
||||
final List<CpuStatus> cpus = [];
|
||||
|
||||
for (var item in raw.split('\n')) {
|
||||
@@ -266,47 +286,31 @@ class ServerProvider extends BusyProvider {
|
||||
int.parse(matches[5]),
|
||||
int.parse(matches[6])));
|
||||
}
|
||||
if (cpus.isEmpty) {
|
||||
info.status.cpu2Status = emptyCpu2Status;
|
||||
} else {
|
||||
|
||||
info.status.cpu2Status =
|
||||
info.status.cpu2Status.update(cpus, _getCPUTemp(temp));
|
||||
if (cpus.isNotEmpty) {
|
||||
info.status.cpu2Status.update(cpus, _getCPUTemp(tempType, tempValue));
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _getUpTime(ServerInfo info) async {
|
||||
final idx = _servers.indexOf(info);
|
||||
final client = _servers[idx].client!;
|
||||
final raw = utf8.decode(await client.run('uptime'));
|
||||
info.status.uptime = raw.split('up ')[1].split(', ')[0];
|
||||
notifyListeners();
|
||||
void _getUpTime(ServerPrivateInfo spi, String raw) {
|
||||
_servers.firstWhere((e) => e.info == spi).status.uptime =
|
||||
raw.split('up ')[1].split(', ')[0];
|
||||
}
|
||||
|
||||
Future<void> _getTcp(ServerInfo info) async {
|
||||
final sidx = _servers.indexOf(info);
|
||||
final client = _servers[sidx].client!;
|
||||
final raw = utf8.decode(await client.run('cat /proc/net/snmp'));
|
||||
void _getTcp(ServerPrivateInfo spi, String raw) {
|
||||
final info = _servers.firstWhere((e) => e.info == spi);
|
||||
final lines = raw.split('\n');
|
||||
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
|
||||
orElse: () => '');
|
||||
if (idx != '') {
|
||||
final vals = idx.split(RegExp(r'\s{1,}'));
|
||||
info.status.tcp = TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
|
||||
} else {
|
||||
info.status.tcp = TcpStatus(0, 0, 0, 0);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _getDisk(ServerInfo info) async {
|
||||
final idx = _servers.indexOf(info);
|
||||
final client = _servers[idx].client!;
|
||||
final disk = utf8.decode(await client.run('df -h'));
|
||||
void _getDisk(ServerPrivateInfo spi, String raw) {
|
||||
final info = _servers.firstWhere((e) => e.info == spi);
|
||||
final list = <DiskInfo>[];
|
||||
final items = disk.split('\n');
|
||||
final items = raw.split('\n');
|
||||
for (var item in items) {
|
||||
if (items.indexOf(item) == 0 || item.isEmpty) {
|
||||
continue;
|
||||
@@ -316,16 +320,13 @@ class ServerProvider extends BusyProvider {
|
||||
int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3]));
|
||||
}
|
||||
info.status.disk = list;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _getMem(ServerInfo info) async {
|
||||
final idx = _servers.indexOf(info);
|
||||
final client = _servers[idx].client!;
|
||||
final mem = utf8.decode(await client.run('free -m'));
|
||||
for (var item in mem.split('\n')) {
|
||||
if (item.contains('Mem:')) {
|
||||
final split = item.replaceFirst('Mem:', '').split(' ');
|
||||
void _getMem(ServerPrivateInfo spi, String raw) {
|
||||
final info = _servers.firstWhere((e) => e.info == spi);
|
||||
for (var item in raw.split('\n')) {
|
||||
if (item.contains(memPrefix)) {
|
||||
final split = item.replaceFirst(memPrefix, '').split(' ');
|
||||
split.removeWhere((e) => e == '');
|
||||
final memList = split.map((e) => int.parse(e)).toList();
|
||||
info.status.memory = Memory(
|
||||
@@ -335,13 +336,16 @@ class ServerProvider extends BusyProvider {
|
||||
shared: memList[3],
|
||||
cache: memList[4],
|
||||
avail: memList[5]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<String?> runSnippet(int idx, Snippet snippet) async {
|
||||
final result = await _servers[idx].client!.run(snippet.script);
|
||||
return utf8.decode(result);
|
||||
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
|
||||
return await _servers
|
||||
.firstWhere((element) => element.info == spi)
|
||||
.client!
|
||||
.run(snippet.script)
|
||||
.string;
|
||||
}
|
||||
}
|
||||
|
||||
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/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/store/snippet.dart';
|
||||
@@ -11,22 +13,34 @@ class SnippetProvider extends BusyProvider {
|
||||
_snippets = locator<SnippetStore>().fetch();
|
||||
}
|
||||
|
||||
void addInfo(Snippet snippet) {
|
||||
void add(Snippet snippet) {
|
||||
if (have(snippet)) return;
|
||||
_snippets.add(snippet);
|
||||
locator<SnippetStore>().put(snippet);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void delInfo(Snippet snippet) {
|
||||
_snippets.removeWhere((e) => e.name == snippet.name);
|
||||
void del(Snippet snippet) {
|
||||
if (!have(snippet)) return;
|
||||
_snippets.removeAt(index(snippet));
|
||||
locator<SnippetStore>().delete(snippet);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateInfo(Snippet old, Snippet newOne) {
|
||||
final idx = _snippets.indexWhere((e) => e.name == old.name);
|
||||
_snippets[idx] = newOne;
|
||||
int index(Snippet snippet) {
|
||||
return _snippets.indexWhere((e) => e.name == snippet.name);
|
||||
}
|
||||
|
||||
bool have(Snippet snippet) {
|
||||
return index(snippet) != -1;
|
||||
}
|
||||
|
||||
void update(Snippet old, Snippet newOne) {
|
||||
if (!have(old)) return;
|
||||
_snippets[index(old)] = newOne;
|
||||
locator<SnippetStore>().update(old, newOne);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String get export => json.encode(snippets);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// This file is generated by ./make.dart
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ToolBox";
|
||||
static const int build = 82;
|
||||
static const String engine = "Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (4 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
|
||||
static const String buildAt = "2022-01-11 13:31:16.586591";
|
||||
static const int modifications = 3;
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 136;
|
||||
static const String engine =
|
||||
"Flutter 3.0.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision ee4e09cce0 (9 days ago) • 2022-05-09 16:45:18 -0700\nEngine • revision d1b9a6938a\nTools • Dart 2.17.0 • DevTools 2.12.2\n";
|
||||
static const String buildAt = "2022-05-19 15:59:10.728748";
|
||||
static const int modifications = 15;
|
||||
}
|
||||
|
||||
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);
|
||||
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,6 @@
|
||||
const backendUrl = 'https://res.lolli.tech';
|
||||
const baseUrl = backendUrl + '/toolbox';
|
||||
const baseUrl = '$backendUrl/toolbox';
|
||||
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';
|
||||
const myGithub = 'https://github.com/LollipopKit';
|
||||
const rainSunMeGithub = 'https://github.com/RainSunMe';
|
||||
const issueUrl = 'https://github.com/LollipopKit/flutter_server_box/issues';
|
||||
|
||||
@@ -23,7 +23,11 @@ class ServerStore extends PersistentStore {
|
||||
|
||||
void update(ServerPrivateInfo old, ServerPrivateInfo newInfo) {
|
||||
final ss = fetch();
|
||||
ss[index(old)] = newInfo;
|
||||
final idx = index(old);
|
||||
if (idx < 0) {
|
||||
throw RangeError.index(idx, ss);
|
||||
}
|
||||
ss[idx] = newInfo;
|
||||
box.put('servers', json.encode(ss));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@ class SettingStore extends PersistentStore {
|
||||
StoreProperty<int> get primaryColor =>
|
||||
property('primaryColor', defaultValue: Colors.deepPurpleAccent.value);
|
||||
StoreProperty<int> get serverStatusUpdateInterval =>
|
||||
property('serverStatusUpdateInterval', defaultValue: 3);
|
||||
property('serverStatusUpdateInterval', defaultValue: 2);
|
||||
StoreProperty<int> get launchPage => property('launchPage', defaultValue: 0);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
213
lib/generated/intl/messages_en.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a en locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'en';
|
||||
|
||||
static String m0(rainSunMeGithub) =>
|
||||
"\nThanks ${rainSunMeGithub} for participating in the test.\n\nAll rights reserved.";
|
||||
|
||||
static String m1(fileName) => "Download [${fileName}] to local?";
|
||||
|
||||
static String m2(runningCount, stoppedCount) =>
|
||||
"${runningCount} running, ${stoppedCount} container stopped.";
|
||||
|
||||
static String m3(count) => "${count} container running.";
|
||||
|
||||
static String m4(percent, size) => "${percent}% of ${size}";
|
||||
|
||||
static String m5(count) => "Found ${count} update";
|
||||
|
||||
static String m6(code) => "request failed, status code: ${code}";
|
||||
|
||||
static String m7(url) =>
|
||||
"Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don\'t have the above issues, please submit an issue on ${url}.";
|
||||
|
||||
static String m8(myGithub) => "\nMade with ❤️ by ${myGithub}";
|
||||
|
||||
static String m9(url) => "Please report bugs on ${url}";
|
||||
|
||||
static String m10(time) => "Spent time: ${time}";
|
||||
|
||||
static String m11(name) => "Are you sure to delete [${name}]?";
|
||||
|
||||
static String m12(server) => "Are you sure to delete server [${server}]?";
|
||||
|
||||
static String m13(build) => "Found: v1.0.${build}, click to update";
|
||||
|
||||
static String m14(build) => "Current: v1.0.${build}";
|
||||
|
||||
static String m15(build) => "Current: v1.0.${build}, is up to date";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"aboutThanks": m0,
|
||||
"addAServer": MessageLookupByLibrary.simpleMessage("add a server"),
|
||||
"addPrivateKey":
|
||||
MessageLookupByLibrary.simpleMessage("Add private key"),
|
||||
"alreadyLastDir":
|
||||
MessageLookupByLibrary.simpleMessage("Already in last directory."),
|
||||
"appPrimaryColor":
|
||||
MessageLookupByLibrary.simpleMessage("App primary color"),
|
||||
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
|
||||
"backDir": MessageLookupByLibrary.simpleMessage("Back"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
|
||||
"chooseDestination":
|
||||
MessageLookupByLibrary.simpleMessage("Choose destination"),
|
||||
"choosePrivateKey":
|
||||
MessageLookupByLibrary.simpleMessage("Choose private key"),
|
||||
"clear": MessageLookupByLibrary.simpleMessage("Clear"),
|
||||
"close": MessageLookupByLibrary.simpleMessage("Close"),
|
||||
"containerStatus":
|
||||
MessageLookupByLibrary.simpleMessage("Container status"),
|
||||
"convert": MessageLookupByLibrary.simpleMessage("Convert"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
|
||||
"copyPath": MessageLookupByLibrary.simpleMessage("Copy path"),
|
||||
"createFile": MessageLookupByLibrary.simpleMessage("Create file"),
|
||||
"createFolder": MessageLookupByLibrary.simpleMessage("Create folder"),
|
||||
"currentMode": MessageLookupByLibrary.simpleMessage("Current Mode"),
|
||||
"debug": MessageLookupByLibrary.simpleMessage("Debug"),
|
||||
"decode": MessageLookupByLibrary.simpleMessage("Decode"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
|
||||
"disconnected": MessageLookupByLibrary.simpleMessage("Disconnected"),
|
||||
"dl2Local": m1,
|
||||
"dockerStatusRunningAndStoppedFmt": m2,
|
||||
"dockerStatusRunningFmt": m3,
|
||||
"download": MessageLookupByLibrary.simpleMessage("Download"),
|
||||
"downloadFinished":
|
||||
MessageLookupByLibrary.simpleMessage("Download finished"),
|
||||
"downloadStatus": m4,
|
||||
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
|
||||
"encode": MessageLookupByLibrary.simpleMessage("Encode"),
|
||||
"error": MessageLookupByLibrary.simpleMessage("Error"),
|
||||
"exampleName": MessageLookupByLibrary.simpleMessage("Example name"),
|
||||
"experimentalFeature":
|
||||
MessageLookupByLibrary.simpleMessage("Experimental feature"),
|
||||
"export": MessageLookupByLibrary.simpleMessage("Export"),
|
||||
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage(
|
||||
"These fields must not be empty."),
|
||||
"files": MessageLookupByLibrary.simpleMessage("Files"),
|
||||
"foundNUpdate": m5,
|
||||
"go": MessageLookupByLibrary.simpleMessage("Go"),
|
||||
"goSftpDlPage":
|
||||
MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"),
|
||||
"host": MessageLookupByLibrary.simpleMessage("Host"),
|
||||
"httpFailedWithCode": m6,
|
||||
"import": MessageLookupByLibrary.simpleMessage("Import"),
|
||||
"importAndExport":
|
||||
MessageLookupByLibrary.simpleMessage("Import and Export"),
|
||||
"install": MessageLookupByLibrary.simpleMessage("install"),
|
||||
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
||||
"Please https://docs.docker.com/engine/install docker first."),
|
||||
"invalidVersionHelp": m7,
|
||||
"keepForeground":
|
||||
MessageLookupByLibrary.simpleMessage("Keep app foreground!"),
|
||||
"keyAuth": MessageLookupByLibrary.simpleMessage("Key Auth"),
|
||||
"lastTry": MessageLookupByLibrary.simpleMessage("Last try!"),
|
||||
"launchPage": MessageLookupByLibrary.simpleMessage("Launch page"),
|
||||
"license": MessageLookupByLibrary.simpleMessage("License"),
|
||||
"loadingFiles":
|
||||
MessageLookupByLibrary.simpleMessage("Loading files..."),
|
||||
"loss": MessageLookupByLibrary.simpleMessage("Loss"),
|
||||
"madeWithLove": m8,
|
||||
"max": MessageLookupByLibrary.simpleMessage("max"),
|
||||
"min": MessageLookupByLibrary.simpleMessage("min"),
|
||||
"ms": MessageLookupByLibrary.simpleMessage("ms"),
|
||||
"name": MessageLookupByLibrary.simpleMessage("Name"),
|
||||
"noClient": MessageLookupByLibrary.simpleMessage("No client"),
|
||||
"noInterface": MessageLookupByLibrary.simpleMessage("No interface"),
|
||||
"noResult": MessageLookupByLibrary.simpleMessage("No result"),
|
||||
"noSavedPrivateKey":
|
||||
MessageLookupByLibrary.simpleMessage("No saved private keys."),
|
||||
"noSavedSnippet":
|
||||
MessageLookupByLibrary.simpleMessage("No saved snippets."),
|
||||
"noServerAvailable":
|
||||
MessageLookupByLibrary.simpleMessage("No server available."),
|
||||
"noUpdateAvailable":
|
||||
MessageLookupByLibrary.simpleMessage("No update available"),
|
||||
"ok": MessageLookupByLibrary.simpleMessage("OK"),
|
||||
"open": MessageLookupByLibrary.simpleMessage("Open"),
|
||||
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
|
||||
"pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"),
|
||||
"pingInputIP": MessageLookupByLibrary.simpleMessage(
|
||||
"Please input a target IP/domain."),
|
||||
"platformNotSupportUpdate": MessageLookupByLibrary.simpleMessage(
|
||||
"Current platform does not support in app update.\nPlease build from source and install it."),
|
||||
"plzEnterHost":
|
||||
MessageLookupByLibrary.simpleMessage("Please enter host."),
|
||||
"plzEnterPwd":
|
||||
MessageLookupByLibrary.simpleMessage("Please enter password."),
|
||||
"plzSelectKey":
|
||||
MessageLookupByLibrary.simpleMessage("Please select a key."),
|
||||
"port": MessageLookupByLibrary.simpleMessage("Port"),
|
||||
"privateKey": MessageLookupByLibrary.simpleMessage("Private Key"),
|
||||
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
|
||||
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
|
||||
"reportBugsOnGithubIssue": m9,
|
||||
"result": MessageLookupByLibrary.simpleMessage("Result"),
|
||||
"run": MessageLookupByLibrary.simpleMessage("Run"),
|
||||
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
||||
"second": MessageLookupByLibrary.simpleMessage("s"),
|
||||
"server": MessageLookupByLibrary.simpleMessage("Server"),
|
||||
"serverTabConnecting":
|
||||
MessageLookupByLibrary.simpleMessage("Connecting..."),
|
||||
"serverTabEmpty": MessageLookupByLibrary.simpleMessage(
|
||||
"There is no server.\nClick the fab to add one."),
|
||||
"serverTabFailed": MessageLookupByLibrary.simpleMessage("Failed"),
|
||||
"serverTabLoading": MessageLookupByLibrary.simpleMessage("Loading..."),
|
||||
"serverTabPlzSave": MessageLookupByLibrary.simpleMessage(
|
||||
"Please \'save\' this private key again."),
|
||||
"serverTabUnkown":
|
||||
MessageLookupByLibrary.simpleMessage("Unknown state"),
|
||||
"setting": MessageLookupByLibrary.simpleMessage("Setting"),
|
||||
"sftpDlPrepare":
|
||||
MessageLookupByLibrary.simpleMessage("Preparing to connect..."),
|
||||
"sftpNoDownloadTask":
|
||||
MessageLookupByLibrary.simpleMessage("No download task."),
|
||||
"sftpSSHConnected":
|
||||
MessageLookupByLibrary.simpleMessage("SFTP Connected"),
|
||||
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
|
||||
"spentTime": m10,
|
||||
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
||||
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
|
||||
"sureDelete": m11,
|
||||
"sureToDeleteServer": m12,
|
||||
"ttl": MessageLookupByLibrary.simpleMessage("TTL"),
|
||||
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
|
||||
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
|
||||
"unkownConvertMode":
|
||||
MessageLookupByLibrary.simpleMessage("Unknown convert mode"),
|
||||
"updateAll": MessageLookupByLibrary.simpleMessage("Update all"),
|
||||
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
|
||||
"You set to 0, will not update automatically.\nYou can pull to refresh manually."),
|
||||
"updateServerStatusInterval": MessageLookupByLibrary.simpleMessage(
|
||||
"Server status update interval"),
|
||||
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
|
||||
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
|
||||
"user": MessageLookupByLibrary.simpleMessage("User"),
|
||||
"versionHaveUpdate": m13,
|
||||
"versionUnknownUpdate": m14,
|
||||
"versionUpdated": m15,
|
||||
"waitConnection": MessageLookupByLibrary.simpleMessage(
|
||||
"Please wait for the connection to be established."),
|
||||
"willTakEeffectImmediately":
|
||||
MessageLookupByLibrary.simpleMessage("Will take effect immediately")
|
||||
};
|
||||
}
|
||||
185
lib/generated/intl/messages_zh.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a zh locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'zh';
|
||||
|
||||
static String m0(rainSunMeGithub) =>
|
||||
"\n感谢 ${rainSunMeGithub} 参与软件测试。\n\n保留所有权利。";
|
||||
|
||||
static String m1(fileName) => "下载 [${fileName}] 到本地?";
|
||||
|
||||
static String m2(runningCount, stoppedCount) =>
|
||||
"${runningCount}个正在运行, ${stoppedCount}个已停止";
|
||||
|
||||
static String m3(count) => "${count}个容器正在运行";
|
||||
|
||||
static String m4(percent, size) => "${size} 的 ${percent}%";
|
||||
|
||||
static String m5(count) => "找到 ${count} 个更新";
|
||||
|
||||
static String m6(code) => "请求失败, 状态码: ${code}";
|
||||
|
||||
static String m7(url) =>
|
||||
"请确保正确安装了docker,或者使用的非自编译版本。如果没有以上问题,请在 ${url} 提交问题。";
|
||||
|
||||
static String m8(myGithub) => "\n用❤️制作 by ${myGithub}";
|
||||
|
||||
static String m9(url) => "请到 ${url} 提交问题";
|
||||
|
||||
static String m10(time) => "耗时: ${time}";
|
||||
|
||||
static String m11(name) => "确定删除[${name}]?";
|
||||
|
||||
static String m12(server) => "你确定要删除服务器 [${server}] 吗?";
|
||||
|
||||
static String m13(build) => "找到新版本:v1.0.${build}, 点击更新";
|
||||
|
||||
static String m14(build) => "当前:v1.0.${build}";
|
||||
|
||||
static String m15(build) => "当前:v1.0.${build}, 已是最新版本";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"aboutThanks": m0,
|
||||
"addAServer": MessageLookupByLibrary.simpleMessage("添加服务器"),
|
||||
"addPrivateKey": MessageLookupByLibrary.simpleMessage("添加一个私钥"),
|
||||
"alreadyLastDir": MessageLookupByLibrary.simpleMessage("已经是最上层目录了"),
|
||||
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
|
||||
"attention": MessageLookupByLibrary.simpleMessage("注意"),
|
||||
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"choose": MessageLookupByLibrary.simpleMessage("选择"),
|
||||
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
|
||||
"choosePrivateKey": MessageLookupByLibrary.simpleMessage("选择私钥"),
|
||||
"clear": MessageLookupByLibrary.simpleMessage("清除"),
|
||||
"close": MessageLookupByLibrary.simpleMessage("关闭"),
|
||||
"containerStatus": MessageLookupByLibrary.simpleMessage("容器状态"),
|
||||
"convert": MessageLookupByLibrary.simpleMessage("转换"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("复制到剪切板"),
|
||||
"copyPath": MessageLookupByLibrary.simpleMessage("复制路径"),
|
||||
"createFile": MessageLookupByLibrary.simpleMessage("创建文件"),
|
||||
"createFolder": MessageLookupByLibrary.simpleMessage("创建文件夹"),
|
||||
"currentMode": MessageLookupByLibrary.simpleMessage("当前模式"),
|
||||
"debug": MessageLookupByLibrary.simpleMessage("调试"),
|
||||
"decode": MessageLookupByLibrary.simpleMessage("解码"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
||||
"disconnected": MessageLookupByLibrary.simpleMessage("连接断开"),
|
||||
"dl2Local": m1,
|
||||
"dockerStatusRunningAndStoppedFmt": m2,
|
||||
"dockerStatusRunningFmt": m3,
|
||||
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
||||
"downloadFinished": MessageLookupByLibrary.simpleMessage("下载完成!"),
|
||||
"downloadStatus": m4,
|
||||
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
||||
"encode": MessageLookupByLibrary.simpleMessage("编码"),
|
||||
"error": MessageLookupByLibrary.simpleMessage("出错了"),
|
||||
"exampleName": MessageLookupByLibrary.simpleMessage("名称示例"),
|
||||
"experimentalFeature": MessageLookupByLibrary.simpleMessage("实验性功能"),
|
||||
"export": MessageLookupByLibrary.simpleMessage("导出"),
|
||||
"fieldMustNotEmpty": MessageLookupByLibrary.simpleMessage("这些输入框不能为空。"),
|
||||
"files": MessageLookupByLibrary.simpleMessage("文件"),
|
||||
"foundNUpdate": m5,
|
||||
"go": MessageLookupByLibrary.simpleMessage("开始"),
|
||||
"goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"),
|
||||
"host": MessageLookupByLibrary.simpleMessage("主机"),
|
||||
"httpFailedWithCode": m6,
|
||||
"import": MessageLookupByLibrary.simpleMessage("导入"),
|
||||
"importAndExport": MessageLookupByLibrary.simpleMessage("导入或导出"),
|
||||
"install": MessageLookupByLibrary.simpleMessage("安装"),
|
||||
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
||||
"请先 https://docs.docker.com/engine/install docker"),
|
||||
"invalidVersionHelp": m7,
|
||||
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
|
||||
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
|
||||
"lastTry": MessageLookupByLibrary.simpleMessage("最后尝试"),
|
||||
"launchPage": MessageLookupByLibrary.simpleMessage("启动页"),
|
||||
"license": MessageLookupByLibrary.simpleMessage("开源证书"),
|
||||
"loadingFiles": MessageLookupByLibrary.simpleMessage("正在加载目录。。。"),
|
||||
"loss": MessageLookupByLibrary.simpleMessage("丢包率"),
|
||||
"madeWithLove": m8,
|
||||
"max": MessageLookupByLibrary.simpleMessage("最大"),
|
||||
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
||||
"ms": MessageLookupByLibrary.simpleMessage("毫秒"),
|
||||
"name": MessageLookupByLibrary.simpleMessage("名称"),
|
||||
"noClient": MessageLookupByLibrary.simpleMessage("没有SSH连接"),
|
||||
"noInterface": MessageLookupByLibrary.simpleMessage("没有可用的接口"),
|
||||
"noResult": MessageLookupByLibrary.simpleMessage("无结果"),
|
||||
"noSavedPrivateKey": MessageLookupByLibrary.simpleMessage("没有已保存的私钥。"),
|
||||
"noSavedSnippet": MessageLookupByLibrary.simpleMessage("没有已保存的代码片段。"),
|
||||
"noServerAvailable": MessageLookupByLibrary.simpleMessage("没有可用的服务器。"),
|
||||
"noUpdateAvailable": MessageLookupByLibrary.simpleMessage("没有可用更新"),
|
||||
"ok": MessageLookupByLibrary.simpleMessage("好"),
|
||||
"open": MessageLookupByLibrary.simpleMessage("打开"),
|
||||
"ping": MessageLookupByLibrary.simpleMessage("Ping"),
|
||||
"pingAvg": MessageLookupByLibrary.simpleMessage("平均:"),
|
||||
"pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"),
|
||||
"platformNotSupportUpdate":
|
||||
MessageLookupByLibrary.simpleMessage("当前平台不支持更新,请编译最新源码后手动安装"),
|
||||
"plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"),
|
||||
"plzEnterPwd": MessageLookupByLibrary.simpleMessage("请输入密码"),
|
||||
"plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"),
|
||||
"port": MessageLookupByLibrary.simpleMessage("端口"),
|
||||
"privateKey": MessageLookupByLibrary.simpleMessage("私钥"),
|
||||
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
|
||||
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
|
||||
"reportBugsOnGithubIssue": m9,
|
||||
"result": MessageLookupByLibrary.simpleMessage("结果"),
|
||||
"run": MessageLookupByLibrary.simpleMessage("运行"),
|
||||
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
||||
"second": MessageLookupByLibrary.simpleMessage("秒"),
|
||||
"server": MessageLookupByLibrary.simpleMessage("服务器"),
|
||||
"serverTabConnecting": MessageLookupByLibrary.simpleMessage("连接中..."),
|
||||
"serverTabEmpty":
|
||||
MessageLookupByLibrary.simpleMessage("现在没有服务器。\n点击右下方按钮来添加。"),
|
||||
"serverTabFailed": MessageLookupByLibrary.simpleMessage("失败"),
|
||||
"serverTabLoading": MessageLookupByLibrary.simpleMessage("加载中..."),
|
||||
"serverTabPlzSave": MessageLookupByLibrary.simpleMessage("请再次保存该私钥"),
|
||||
"serverTabUnkown": MessageLookupByLibrary.simpleMessage("未知状态"),
|
||||
"setting": MessageLookupByLibrary.simpleMessage("设置"),
|
||||
"sftpDlPrepare": MessageLookupByLibrary.simpleMessage("准备连接至服务器..."),
|
||||
"sftpNoDownloadTask": MessageLookupByLibrary.simpleMessage("没有下载任务"),
|
||||
"sftpSSHConnected":
|
||||
MessageLookupByLibrary.simpleMessage("SFTP 已连接,即将开始下载..."),
|
||||
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
|
||||
"spentTime": m10,
|
||||
"start": MessageLookupByLibrary.simpleMessage("开始"),
|
||||
"stop": MessageLookupByLibrary.simpleMessage("停止"),
|
||||
"sureDelete": m11,
|
||||
"sureToDeleteServer": m12,
|
||||
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
|
||||
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
||||
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
|
||||
"unkownConvertMode": MessageLookupByLibrary.simpleMessage("未知转换模式"),
|
||||
"updateAll": MessageLookupByLibrary.simpleMessage("更新全部"),
|
||||
"updateIntervalEqual0": MessageLookupByLibrary.simpleMessage(
|
||||
"你设置为0,服务器状态不会自动刷新。\n你可以手动下拉刷新。"),
|
||||
"updateServerStatusInterval":
|
||||
MessageLookupByLibrary.simpleMessage("服务器状态刷新间隔"),
|
||||
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
|
||||
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
|
||||
"user": MessageLookupByLibrary.simpleMessage("用户"),
|
||||
"versionHaveUpdate": m13,
|
||||
"versionUnknownUpdate": m14,
|
||||
"versionUpdated": m15,
|
||||
"waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"),
|
||||
"willTakEeffectImmediately":
|
||||
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
|
||||
};
|
||||
}
|
||||
1250
lib/generated/l10n.dart
Normal file
119
lib/l10n/intl_en.arb
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"server": "Server",
|
||||
"convert": "Convert",
|
||||
"ping": "Ping",
|
||||
"debug": "Debug",
|
||||
"addAServer": "add a server",
|
||||
"setting": "Setting",
|
||||
"license": "License",
|
||||
"snippet": "Snippet",
|
||||
"privateKey": "Private Key",
|
||||
"madeWithLove": "\nMade with ❤️ by {myGithub}",
|
||||
"aboutThanks": "\nThanks {rainSunMeGithub} for participating in the test.\n\nAll rights reserved.",
|
||||
"serverTabEmpty": "There is no server.\nClick the fab to add one.",
|
||||
"serverTabLoading": "Loading...",
|
||||
"serverTabPlzSave": "Please 'save' this private key again.",
|
||||
"serverTabFailed": "Failed",
|
||||
"serverTabUnkown": "Unknown state",
|
||||
"serverTabConnecting": "Connecting...",
|
||||
"decode": "Decode",
|
||||
"encode": "Encode",
|
||||
"currentMode": "Current Mode",
|
||||
"unkownConvertMode": "Unknown convert mode",
|
||||
"copy": "Copy",
|
||||
"upsideDown": "Upside Down",
|
||||
"pingAvg": "Avg:",
|
||||
"unknown": "unknown",
|
||||
"min": "min",
|
||||
"max": "max",
|
||||
"ms": "ms",
|
||||
"ttl": "TTL",
|
||||
"loss": "Loss",
|
||||
"noResult": "No result",
|
||||
"pingInputIP": "Please input a target IP/domain.",
|
||||
"clear": "Clear",
|
||||
"start": "Start",
|
||||
"appPrimaryColor": "App primary color",
|
||||
"updateServerStatusInterval": "Server status update interval",
|
||||
"willTakEeffectImmediately": "Will take effect immediately",
|
||||
"launchPage": "Launch page",
|
||||
"versionUpdated": "Current: v1.0.{build}, is up to date",
|
||||
"versionUnknownUpdate": "Current: v1.0.{build}",
|
||||
"versionHaveUpdate": "Found: v1.0.{build}, click to update",
|
||||
"second": "s",
|
||||
"updateIntervalEqual0": "You set to 0, will not update automatically.\nYou can pull to refresh manually.",
|
||||
"edit": "Edit",
|
||||
"noSavedPrivateKey": "No saved private keys.",
|
||||
"name": "Name",
|
||||
"pwd": "Password",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"fieldMustNotEmpty": "These fields must not be empty.",
|
||||
"importAndExport": "Import and Export",
|
||||
"choose": "Choose",
|
||||
"import": "Import",
|
||||
"export": "Export",
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel",
|
||||
"urlOrJson": "URL or JSON",
|
||||
"go": "Go",
|
||||
"httpFailedWithCode": "request failed, status code: {code}",
|
||||
"run": "Run",
|
||||
"noSavedSnippet": "No saved snippets.",
|
||||
"chooseDestination": "Choose destination",
|
||||
"noServerAvailable": "No server available.",
|
||||
"result": "Result",
|
||||
"close": "Close",
|
||||
"attention": "Attention",
|
||||
"sureToDeleteServer": "Are you sure to delete server [{server}]?",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"user": "User",
|
||||
"keyAuth": "Key Auth",
|
||||
"addPrivateKey": "Add private key",
|
||||
"choosePrivateKey": "Choose private key",
|
||||
"plzEnterHost": "Please enter host.",
|
||||
"plzEnterPwd": "Please enter password.",
|
||||
"plzSelectKey": "Please select a key.",
|
||||
"exampleName": "Example name",
|
||||
"stop": "Stop",
|
||||
"download": "Download",
|
||||
"copyPath": "Copy path",
|
||||
"keepForeground": "Keep app foreground!",
|
||||
"downloadFinished": "Download finished",
|
||||
"downloadStatus": "{percent}% of {size}",
|
||||
"sftpDlPrepare": "Preparing to connect...",
|
||||
"sftpSSHConnected": "SFTP Connected",
|
||||
"spentTime": "Spent time: {time}",
|
||||
"backDir": "Back",
|
||||
"alreadyLastDir": "Already in last directory.",
|
||||
"open": "Open",
|
||||
"sureDelete": "Are you sure to delete [{name}]?",
|
||||
"containerStatus": "Container status",
|
||||
"noClient": "No client",
|
||||
"installDockerWithUrl": "Please https://docs.docker.com/engine/install docker first.",
|
||||
"waitConnection": "Please wait for the connection to be established.",
|
||||
"unknownError": "Unknown error",
|
||||
"dockerStatusRunningFmt": "{count} container running.",
|
||||
"dockerStatusRunningAndStoppedFmt": "{runningCount} running, {stoppedCount} container stopped.",
|
||||
"install": "install",
|
||||
"loadingFiles": "Loading files...",
|
||||
"sftpNoDownloadTask": "No download task.",
|
||||
"goSftpDlPage": "Go to SFTP download page?",
|
||||
"createFolder": "Create folder",
|
||||
"createFile": "Create file",
|
||||
"rename": "Rename",
|
||||
"dl2Local": "Download [{fileName}] to local?",
|
||||
"error": "Error",
|
||||
"disconnected": "Disconnected",
|
||||
"files": "Files",
|
||||
"experimentalFeature": "Experimental feature",
|
||||
"reportBugsOnGithubIssue": "Please report bugs on {url}",
|
||||
"noUpdateAvailable": "No update available",
|
||||
"foundNUpdate": "Found {count} update",
|
||||
"updateAll": "Update all",
|
||||
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
|
||||
"invalidVersionHelp": "Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don't have the above issues, please submit an issue on {url}.",
|
||||
"noInterface": "No interface",
|
||||
"lastTry": "Last try!"
|
||||
}
|
||||
119
lib/l10n/intl_zh.arb
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"server": "服务器",
|
||||
"convert": "转换",
|
||||
"ping": "Ping",
|
||||
"debug": "调试",
|
||||
"addAServer": "添加服务器",
|
||||
"setting": "设置",
|
||||
"license": "开源证书",
|
||||
"snippet": "代码片段",
|
||||
"privateKey": "私钥",
|
||||
"madeWithLove": "\n用❤️制作 by {myGithub}",
|
||||
"aboutThanks": "\n感谢 {rainSunMeGithub} 参与软件测试。\n\n保留所有权利。",
|
||||
"serverTabEmpty": "现在没有服务器。\n点击右下方按钮来添加。",
|
||||
"serverTabLoading": "加载中...",
|
||||
"serverTabPlzSave": "请再次保存该私钥",
|
||||
"serverTabFailed": "失败",
|
||||
"serverTabUnkown": "未知状态",
|
||||
"serverTabConnecting": "连接中...",
|
||||
"decode": "解码",
|
||||
"encode": "编码",
|
||||
"currentMode": "当前模式",
|
||||
"unkownConvertMode": "未知转换模式",
|
||||
"copy": "复制到剪切板",
|
||||
"upsideDown": "上下交换",
|
||||
"pingAvg": "平均:",
|
||||
"unknown": "未知",
|
||||
"min": "最小",
|
||||
"max": "最大",
|
||||
"ms": "毫秒",
|
||||
"ttl": "缓存时间",
|
||||
"loss": "丢包率",
|
||||
"noResult": "无结果",
|
||||
"pingInputIP": "请输入目标IP或域名",
|
||||
"clear": "清除",
|
||||
"start": "开始",
|
||||
"appPrimaryColor": "App主要色",
|
||||
"updateServerStatusInterval": "服务器状态刷新间隔",
|
||||
"willTakEeffectImmediately": "更改将会立即生效",
|
||||
"launchPage": "启动页",
|
||||
"versionUpdated": "当前:v1.0.{build}, 已是最新版本",
|
||||
"versionUnknownUpdate": "当前:v1.0.{build}",
|
||||
"versionHaveUpdate": "找到新版本:v1.0.{build}, 点击更新",
|
||||
"second": "秒",
|
||||
"updateIntervalEqual0": "你设置为0,服务器状态不会自动刷新。\n你可以手动下拉刷新。",
|
||||
"edit": "编辑",
|
||||
"noSavedPrivateKey": "没有已保存的私钥。",
|
||||
"name": "名称",
|
||||
"pwd": "密码",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"fieldMustNotEmpty": "这些输入框不能为空。",
|
||||
"importAndExport": "导入或导出",
|
||||
"choose": "选择",
|
||||
"import": "导入",
|
||||
"export": "导出",
|
||||
"ok": "好",
|
||||
"cancel": "取消",
|
||||
"urlOrJson": "链接或JSON",
|
||||
"go": "开始",
|
||||
"httpFailedWithCode": "请求失败, 状态码: {code}",
|
||||
"run": "运行",
|
||||
"noSavedSnippet": "没有已保存的代码片段。",
|
||||
"chooseDestination": "选择目标",
|
||||
"noServerAvailable": "没有可用的服务器。",
|
||||
"result": "结果",
|
||||
"close": "关闭",
|
||||
"attention": "注意",
|
||||
"sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?",
|
||||
"host": "主机",
|
||||
"port": "端口",
|
||||
"user": "用户",
|
||||
"keyAuth": "公钥认证",
|
||||
"addPrivateKey": "添加一个私钥",
|
||||
"choosePrivateKey": "选择私钥",
|
||||
"plzEnterHost": "请输入主机",
|
||||
"plzEnterPwd": "请输入密码",
|
||||
"plzSelectKey": "请选择私钥",
|
||||
"exampleName": "名称示例",
|
||||
"stop": "停止",
|
||||
"download": "下载",
|
||||
"copyPath": "复制路径",
|
||||
"keepForeground": "请保持应用处于前台!",
|
||||
"downloadFinished": "下载完成!",
|
||||
"downloadStatus": "{size} 的 {percent}%",
|
||||
"sftpDlPrepare": "准备连接至服务器...",
|
||||
"sftpSSHConnected": "SFTP 已连接,即将开始下载...",
|
||||
"spentTime": "耗时: {time}",
|
||||
"backDir": "返回上一级",
|
||||
"alreadyLastDir": "已经是最上层目录了",
|
||||
"open": "打开",
|
||||
"sureDelete": "确定删除[{name}]?",
|
||||
"containerStatus": "容器状态",
|
||||
"noClient": "没有SSH连接",
|
||||
"installDockerWithUrl": "请先 https://docs.docker.com/engine/install docker",
|
||||
"waitConnection": "请等待连接建立",
|
||||
"unknownError": "未知错误",
|
||||
"dockerStatusRunningFmt": "{count}个容器正在运行",
|
||||
"dockerStatusRunningAndStoppedFmt": "{runningCount}个正在运行, {stoppedCount}个已停止",
|
||||
"install": "安装",
|
||||
"loadingFiles": "正在加载目录。。。",
|
||||
"sftpNoDownloadTask": "没有下载任务",
|
||||
"goSftpDlPage": "前往下载页?",
|
||||
"createFolder": "创建文件夹",
|
||||
"createFile": "创建文件",
|
||||
"rename": "重命名",
|
||||
"dl2Local": "下载 [{fileName}] 到本地?",
|
||||
"error": "出错了",
|
||||
"disconnected": "连接断开",
|
||||
"files": "文件",
|
||||
"experimentalFeature": "实验性功能",
|
||||
"reportBugsOnGithubIssue": "请到 {url} 提交问题",
|
||||
"noUpdateAvailable": "没有可用更新",
|
||||
"foundNUpdate": "找到 {count} 个更新",
|
||||
"updateAll": "更新全部",
|
||||
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
|
||||
"invalidVersionHelp": "请确保正确安装了docker,或者使用的非自编译版本。如果没有以上问题,请在 {url} 提交问题。",
|
||||
"noInterface": "没有可用的接口",
|
||||
"lastTry": "最后尝试"
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/apt.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/provider/docker.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/sftp_download.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/data/service/app.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
@@ -18,10 +21,13 @@ void setupLocatorForServices() {
|
||||
|
||||
void setupLocatorForProviders() {
|
||||
locator.registerSingleton(AppProvider());
|
||||
locator.registerSingleton(AptProvider());
|
||||
locator.registerSingleton(DebugProvider());
|
||||
locator.registerSingleton(DockerProvider());
|
||||
locator.registerSingleton(ServerProvider());
|
||||
locator.registerSingleton(SnippetProvider());
|
||||
locator.registerSingleton(PrivateKeyProvider());
|
||||
locator.registerSingleton(SftpDownloadProvider());
|
||||
}
|
||||
|
||||
Future<void> setupLocatorForStores() async {
|
||||
|
||||
@@ -7,9 +7,12 @@ import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/app.dart';
|
||||
import 'package:toolbox/core/analysis.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/apt.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/provider/docker.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/sftp_download.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
@@ -62,10 +65,14 @@ Future<void> main() async {
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<AptProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<DockerProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => locator<SftpDownloadProvider>()),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
|
||||
236
lib/view/page/apt.dart
Normal file
@@ -0,0 +1,236 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/apt.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/center_loading.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||
import 'package:toolbox/view/widget/url_text.dart';
|
||||
|
||||
class AptManagePage extends StatefulWidget {
|
||||
const AptManagePage(this.spi, {Key? key}) : super(key: key);
|
||||
|
||||
final ServerPrivateInfo spi;
|
||||
|
||||
@override
|
||||
_AptManagePageState createState() => _AptManagePageState();
|
||||
}
|
||||
|
||||
class _AptManagePageState extends State<AptManagePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late MediaQueryData _media;
|
||||
final greyStyle = const TextStyle(color: Colors.grey);
|
||||
final scrollController = ScrollController();
|
||||
final scrollControllerUpdate = ScrollController();
|
||||
final _aptProvider = locator<AptProvider>();
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
s = S.of(context);
|
||||
_aptProvider.refreshInstalled();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
locator<AptProvider>().clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final si = locator<ServerProvider>()
|
||||
.servers
|
||||
.firstWhere((e) => e.info == widget.spi);
|
||||
if (si.client == null) {
|
||||
showSnackBar(context, Text(s.waitConnection));
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
PwdRequestFunc onPwdRequest = (lastTime, user) async {
|
||||
if (!mounted) return '';
|
||||
final textController = TextEditingController();
|
||||
await showRoundDialog(
|
||||
context,
|
||||
lastTime ? s.lastTry : (user ?? s.unknown),
|
||||
TextField(
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
onSubmitted: (_) => textController.text.trim(),
|
||||
decoration: InputDecoration(
|
||||
labelText: s.pwd,
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(
|
||||
context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.ok)),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
return textController.text.trim();
|
||||
};
|
||||
|
||||
_aptProvider.init(
|
||||
si.client!,
|
||||
si.status.sysVer.dist,
|
||||
() =>
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent),
|
||||
() => scrollControllerUpdate
|
||||
.jumpTo(scrollControllerUpdate.position.maxScrollExtent),
|
||||
onPwdRequest);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: 'Apt', down: widget.spi.name),
|
||||
),
|
||||
body: Consumer<AptProvider>(builder: (_, apt, __) {
|
||||
if (apt.error != null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error,
|
||||
color: Colors.redAccent,
|
||||
size: 37,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 37,
|
||||
),
|
||||
Text(
|
||||
apt.error!,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (apt.updateLog == null && apt.upgradeable == null) {
|
||||
return centerLoading;
|
||||
}
|
||||
if (apt.updateLog != null && apt.upgradeable == null) {
|
||||
return SizedBox(
|
||||
height: _media.size.height * 0.7,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
controller: scrollControllerUpdate,
|
||||
child: Text(apt.updateLog!),
|
||||
),
|
||||
));
|
||||
}
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: UrlText(
|
||||
text:
|
||||
'${s.experimentalFeature}\n${s.reportBugsOnGithubIssue(issueUrl)}',
|
||||
replace: 'Github Issue',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
_buildUpdatePanel(apt)
|
||||
].map((e) => RoundRectCard(e)).toList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUpdatePanel(AptProvider apt) {
|
||||
if (apt.upgradeable!.isEmpty) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
s.noUpdateAvailable,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
subtitle: const Text('>_<', textAlign: TextAlign.center),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ExpansionTile(
|
||||
title: Text(s.foundNUpdate(apt.upgradeable!.length)),
|
||||
subtitle: Text(
|
||||
apt.upgradeable!.map((e) => e.package).join(', '),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: greyStyle,
|
||||
),
|
||||
children: apt.upgradeLog == null
|
||||
? [
|
||||
TextButton(
|
||||
child: Text(s.updateAll),
|
||||
onPressed: () {
|
||||
apt.upgrade();
|
||||
}),
|
||||
SizedBox(
|
||||
height: _media.size.height * 0.73,
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
children: apt.upgradeable!
|
||||
.map((e) => _buildUpdateItem(e, apt))
|
||||
.toList()),
|
||||
)
|
||||
]
|
||||
: [
|
||||
SizedBox(
|
||||
height: _media.size.height * 0.7,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
controller: scrollController,
|
||||
child: Text(apt.upgradeLog!),
|
||||
),
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUpdateItem(UpgradePkgInfo info, AptProvider apt) {
|
||||
return ListTile(
|
||||
title: Text(info.package),
|
||||
subtitle: Text(
|
||||
'${info.nowVersion} -> ${info.newVersion}',
|
||||
style: greyStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import 'package:clipboard/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
class ConvertPage extends StatefulWidget {
|
||||
const ConvertPage({Key? key}) : super(key: key);
|
||||
@@ -18,13 +21,8 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
late TextEditingController _textEditingControllerResult;
|
||||
late MediaQueryData _media;
|
||||
late ThemeData _theme;
|
||||
late S s;
|
||||
|
||||
static const List<String> _typeOption = [
|
||||
'base64 decode',
|
||||
'base64 encode',
|
||||
'URL encode',
|
||||
'URL decode'
|
||||
];
|
||||
int _typeOptionIndex = 0;
|
||||
|
||||
@override
|
||||
@@ -39,6 +37,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
_theme = Theme.of(context);
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -48,6 +47,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
body: GestureDetector(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
controller: ScrollController(),
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 13),
|
||||
_buildInputTop(),
|
||||
@@ -64,7 +64,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
showSnackBar(context, Text('Error: \n$e'));
|
||||
}
|
||||
},
|
||||
tooltip: 'convert',
|
||||
tooltip: s.convert,
|
||||
child: const Icon(Icons.send),
|
||||
),
|
||||
);
|
||||
@@ -82,20 +82,29 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
case 3:
|
||||
return Uri.decodeFull(text);
|
||||
default:
|
||||
return 'Unknown';
|
||||
return s.unkownConvertMode;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInputTop() {
|
||||
return SizedBox(
|
||||
height: _media.size.height * 0.33,
|
||||
child: _buildInput(_textEditingController),
|
||||
child: buildInput(context, _textEditingController,
|
||||
onSubmitted: (_) => _textEditingControllerResult.text = doConvert()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeOption() {
|
||||
return Card(
|
||||
child: ExpansionTile(
|
||||
final decode = s.decode;
|
||||
final encode = s.encode;
|
||||
final List<String> typeOption = [
|
||||
'Base64 $decode',
|
||||
'Base64 $encode',
|
||||
'URL $encode',
|
||||
'URL $decode'
|
||||
];
|
||||
return RoundRectCard(
|
||||
ExpansionTile(
|
||||
tilePadding: const EdgeInsets.only(left: 7, right: 27),
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
title: Row(
|
||||
@@ -103,7 +112,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(primaryColor)),
|
||||
child: const Icon(Icons.change_circle),
|
||||
child: Icon(Icons.change_circle, semanticLabel: s.upsideDown),
|
||||
onPressed: () {
|
||||
final temp = _textEditingController.text;
|
||||
_textEditingController.text = _textEditingControllerResult.text;
|
||||
@@ -113,7 +122,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(primaryColor)),
|
||||
child: const Icon(Icons.copy),
|
||||
child: Icon(Icons.copy, semanticLabel: s.copy),
|
||||
onPressed: () => FlutterClipboard.copy(
|
||||
_textEditingControllerResult.text == ''
|
||||
? ' '
|
||||
@@ -127,23 +136,23 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_typeOption[_typeOptionIndex],
|
||||
Text(typeOption[_typeOptionIndex],
|
||||
textScaleFactor: 1.0,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: primaryColor)),
|
||||
const Text(
|
||||
'Current Mode',
|
||||
Text(
|
||||
s.currentMode,
|
||||
textScaleFactor: 1.0,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(fontSize: 9.0, color: Colors.grey),
|
||||
style: const TextStyle(fontSize: 9.0, color: Colors.grey),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
children: _typeOption
|
||||
children: typeOption
|
||||
.map((e) => ListTile(
|
||||
title: Text(
|
||||
e,
|
||||
@@ -151,7 +160,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
color:
|
||||
_theme.textTheme.bodyText2!.color!.withAlpha(177)),
|
||||
),
|
||||
trailing: _buildRadio(_typeOption.indexOf(e)),
|
||||
trailing: _buildRadio(typeOption.indexOf(e)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
@@ -161,20 +170,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
Widget _buildResult() {
|
||||
return SizedBox(
|
||||
height: _media.size.height * 0.33,
|
||||
child: _buildInput(_textEditingControllerResult),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInput(TextEditingController controller) {
|
||||
return Card(
|
||||
child: TextField(
|
||||
maxLines: 20,
|
||||
decoration: InputDecoration(
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
filled: true,
|
||||
border: InputBorder.none),
|
||||
controller: controller,
|
||||
),
|
||||
child: buildInput(context, _textEditingControllerResult),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class _DebugPageState extends State<DebugPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar:
|
||||
AppBar(title: const Text('Terminal'), backgroundColor: Colors.black),
|
||||
AppBar(title: const Text('App log'), backgroundColor: Colors.black),
|
||||
body: _buildTerminal(context),
|
||||
backgroundColor: Colors.black,
|
||||
);
|
||||
|
||||
209
lib/view/page/docker.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/app/menu_item.dart';
|
||||
import 'package:toolbox/data/model/docker/ps.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/docker.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/center_loading.dart';
|
||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
import 'package:toolbox/view/widget/url_text.dart';
|
||||
|
||||
class DockerManagePage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
const DockerManagePage(this.spi, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DockerManagePage> createState() => _DockerManagePageState();
|
||||
}
|
||||
|
||||
class _DockerManagePageState extends State<DockerManagePage> {
|
||||
final _docker = locator<DockerProvider>();
|
||||
final greyTextStyle = const TextStyle(color: Colors.grey);
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_docker.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final client = locator<ServerProvider>()
|
||||
.servers
|
||||
.firstWhere((element) => element.info == widget.spi)
|
||||
.client;
|
||||
if (client == null) {
|
||||
showSnackBar(context, Text(s.noClient));
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
_docker.init(client);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: 'Docker', down: widget.spi.name),
|
||||
),
|
||||
body: _buildMain(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMain() {
|
||||
return Consumer<DockerProvider>(builder: (_, docker, __) {
|
||||
final running = docker.items;
|
||||
if (docker.error != null && running == null) {
|
||||
return SizedBox.expand(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error,
|
||||
size: 37,
|
||||
),
|
||||
const SizedBox(height: 27),
|
||||
Text(docker.error!),
|
||||
const SizedBox(height: 27),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: _buildSolution(docker.error!),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (running == null) {
|
||||
_docker.refresh();
|
||||
return centerLoading;
|
||||
}
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(7),
|
||||
children: [
|
||||
_buildVersion(
|
||||
docker.edition ?? s.unknown, docker.version ?? s.unknown),
|
||||
_buildPsItems(running, docker)
|
||||
].map((e) => RoundRectCard(e)).toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSolution(String err) {
|
||||
switch (err) {
|
||||
case 'docker not found':
|
||||
return UrlText(
|
||||
text: s.installDockerWithUrl,
|
||||
replace: s.install,
|
||||
);
|
||||
case 'no client':
|
||||
return Text(s.waitConnection);
|
||||
case 'invalid version':
|
||||
return UrlText(
|
||||
text: s.invalidVersionHelp(issueUrl),
|
||||
replace: 'Github',
|
||||
);
|
||||
default:
|
||||
return Text(s.unknownError);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVersion(String edition, String version) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [Text(edition), Text(version)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPsItems(List<DockerPsItem> running, DockerProvider docker) {
|
||||
return ExpansionTile(
|
||||
title: Text(s.containerStatus),
|
||||
subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
|
||||
children: running.map((item) {
|
||||
return ListTile(
|
||||
title: Text(item.image),
|
||||
subtitle: Text(item.status),
|
||||
trailing: docker.isBusy
|
||||
? const CircularProgressIndicator()
|
||||
: _buildMoreBtn(item.running, item.containerId),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoreBtn(bool running, String containerId) {
|
||||
final item = running ? DockerMenuItems.stop : DockerMenuItems.start;
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton2(
|
||||
customButton: const Padding(
|
||||
padding: EdgeInsets.only(left: 7),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
size: 17,
|
||||
),
|
||||
),
|
||||
customItemsHeight: 8,
|
||||
items: [
|
||||
DropdownMenuItem<DropdownBtnItem>(
|
||||
value: item,
|
||||
child: item.build,
|
||||
),
|
||||
DropdownMenuItem<DropdownBtnItem>(
|
||||
value: DockerMenuItems.rm,
|
||||
child: DockerMenuItems.rm.build,
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
final item = value as DropdownBtnItem;
|
||||
switch (item) {
|
||||
case DockerMenuItems.rm:
|
||||
_docker.delete(containerId);
|
||||
break;
|
||||
case DockerMenuItems.start:
|
||||
_docker.start(containerId);
|
||||
break;
|
||||
case DockerMenuItems.stop:
|
||||
_docker.stop(containerId);
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemHeight: 37,
|
||||
itemPadding: const EdgeInsets.only(left: 17, right: 17),
|
||||
dropdownWidth: 133,
|
||||
dropdownDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
dropdownElevation: 8,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _buildSubtitle(List<DockerPsItem> running) {
|
||||
final runningCount = running.where((element) => element.running).length;
|
||||
final stoped = running.length - runningCount;
|
||||
if (stoped == 0) {
|
||||
return s.dockerStatusRunningFmt(runningCount);
|
||||
}
|
||||
return s.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_advanced_drawer/flutter_advanced_drawer.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:toolbox/core/analysis.dart';
|
||||
import 'package:toolbox/core/build_mode.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/core/update.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/app/navigation_item.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/res/icon/common.dart';
|
||||
import 'package:toolbox/data/res/tab.dart';
|
||||
import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/convert.dart';
|
||||
import 'package:toolbox/view/page/debug.dart';
|
||||
import 'package:toolbox/view/page/ping.dart';
|
||||
import 'package:toolbox/view/page/private_key/list.dart';
|
||||
import 'package:toolbox/view/page/server/tab.dart';
|
||||
import 'package:toolbox/view/page/setting.dart';
|
||||
import 'package:toolbox/view/page/sftp/downloaded.dart';
|
||||
import 'package:toolbox/view/page/snippet/list.dart';
|
||||
import 'package:toolbox/view/widget/url_text.dart';
|
||||
|
||||
@@ -33,28 +42,41 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
SingleTickerProviderStateMixin,
|
||||
AfterLayoutMixin,
|
||||
WidgetsBindingObserver {
|
||||
final List<String> _tabs = ['Servers', 'En/Decode'];
|
||||
late final TabController _tabController;
|
||||
late final ServerProvider _serverProvider;
|
||||
late final PageController _pageController;
|
||||
late final AdvancedDrawerController _advancedDrawerController;
|
||||
late int _selectIndex;
|
||||
late double _width;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverProvider = locator<ServerProvider>();
|
||||
WidgetsBinding.instance?.addObserver(this);
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_selectIndex = locator<SettingStore>().launchPage.fetch()!;
|
||||
_pageController = PageController(initialPage: _selectIndex);
|
||||
_advancedDrawerController = AdvancedDrawerController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
_width = MediaQuery.of(context).size.width;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
WidgetsBinding.instance?.removeObserver(this);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.paused) {
|
||||
_serverProvider.setDisconnected();
|
||||
_serverProvider.stopAutoRefresh();
|
||||
}
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
@@ -64,72 +86,183 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
setTransparentNavigationBar(context);
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: GestureDetector(
|
||||
onLongPress: () =>
|
||||
AppRoute(const DebugPage(), 'Debug Page').go(context),
|
||||
child: const Text(BuildData.name),
|
||||
),
|
||||
bottom: TabBar(
|
||||
indicatorColor: widget.primaryColor,
|
||||
tabs: _tabs.map((e) => Tab(text: e)).toList(),
|
||||
controller: _tabController,
|
||||
return WillPopScope(
|
||||
child: _buildMain(context),
|
||||
onWillPop: () {
|
||||
if (_advancedDrawerController.value.visible) {
|
||||
_advancedDrawerController.hideDrawer();
|
||||
return Future.value(false);
|
||||
}
|
||||
return Future.value(true);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildMain(BuildContext context) {
|
||||
return AdvancedDrawer(
|
||||
controller: _advancedDrawerController,
|
||||
animationCurve: Curves.easeInOut,
|
||||
animationDuration: const Duration(milliseconds: 300),
|
||||
animateChildDecoration: true,
|
||||
rtlOpening: false,
|
||||
childDecoration: const BoxDecoration(
|
||||
// NOTICE: Uncomment if you want to add shadow behind the page.
|
||||
// Keep in mind that it may cause animation jerks.
|
||||
// boxShadow: <BoxShadow>[
|
||||
// BoxShadow(
|
||||
// color: Colors.black12,
|
||||
// blurRadius: 0.0,
|
||||
// ),
|
||||
// ],
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
drawer: _buildDrawer(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tabTitleName(context, _selectIndex), style: size18),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.developer_mode, size: 23),
|
||||
tooltip: s.debug,
|
||||
onPressed: () =>
|
||||
AppRoute(const DebugPage(), 'Debug Page').go(context),
|
||||
),
|
||||
],
|
||||
leading: IconButton(
|
||||
onPressed: () => _advancedDrawerController.showDrawer(),
|
||||
icon: ValueListenableBuilder<AdvancedDrawerValue>(
|
||||
valueListenable: _advancedDrawerController,
|
||||
builder: (_, value, __) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Icon(
|
||||
value.visible ? Icons.clear : Icons.menu,
|
||||
key: ValueKey<bool>(value.visible),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
body: PageView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
controller: _pageController,
|
||||
onPageChanged: (i) {
|
||||
FocusScope.of(context).unfocus();
|
||||
_selectIndex = i;
|
||||
setState(() {});
|
||||
},
|
||||
children: const [ServerPage(), ConvertPage(), PingPage()],
|
||||
),
|
||||
bottomNavigationBar: _buildBottom(context),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildItem(int idx, NavigationItem item, bool isSelected) {
|
||||
bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
final width = _width / tabItems.length;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
height: 50,
|
||||
width: isSelected ? width : width - 17,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? isDarkMode
|
||||
? Colors.white12
|
||||
: Colors.black.withOpacity(0.07)
|
||||
: Colors.transparent,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50))),
|
||||
child: IconButton(
|
||||
icon: Icon(item.icon),
|
||||
tooltip: tabTitleName(context, idx),
|
||||
splashRadius: width / 3.3,
|
||||
padding: const EdgeInsets.only(left: 17, right: 17),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_pageController.animateToPage(idx,
|
||||
duration: const Duration(milliseconds: 677),
|
||||
curve: Curves.fastLinearToSlowEaseIn);
|
||||
});
|
||||
},
|
||||
),
|
||||
drawer: _buildDrawer(),
|
||||
body: TabBarView(controller: _tabController, children: const [
|
||||
ServerPage(),
|
||||
ConvertPage(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottom(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.only(left: 8, top: 4, bottom: 4, right: 8),
|
||||
width: _width,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: tabItems.map((item) {
|
||||
int itemIndex = tabItems.indexOf(item);
|
||||
return _buildItem(itemIndex, item, _selectIndex == itemIndex);
|
||||
}).toList(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildDrawer() {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: const Text(BuildData.name),
|
||||
accountEmail: Text(_buildVersionStr()),
|
||||
currentAccountPicture: _buildIcon(),
|
||||
_buildIcon(),
|
||||
const Text(BuildData.name),
|
||||
Text(_buildVersionStr()),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.07,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('Setting'),
|
||||
onTap: () => AppRoute(const SettingPage(), 'Setting').go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: const Text('Private Key'),
|
||||
onTap: () =>
|
||||
AppRoute(const StoredPrivateKeysPage(), 'private key list')
|
||||
.go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.snippet_folder),
|
||||
title: const Text('Snippet'),
|
||||
onTap: () =>
|
||||
AppRoute(const SnippetListPage(), 'snippet list').go(context),
|
||||
),
|
||||
AboutListTile(
|
||||
icon: const Icon(Icons.text_snippet),
|
||||
child: const Text('Licences'),
|
||||
applicationName: BuildData.name,
|
||||
applicationVersion: _buildVersionStr(),
|
||||
applicationIcon: _buildIcon(),
|
||||
aboutBoxChildren: const [
|
||||
UrlText(
|
||||
text: '\nMade with ❤️ by $myGithub', replace: 'LollipopKit'),
|
||||
UrlText(
|
||||
text:
|
||||
'\nThanks $rainSunMeGithub for participating in the test.\n\nAll rights reserved.',
|
||||
replace: 'RainSunMe',
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 29),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(s.setting),
|
||||
onTap: () =>
|
||||
AppRoute(const SettingPage(), 'Setting').go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(s.privateKey),
|
||||
onTap: () => AppRoute(
|
||||
const StoredPrivateKeysPage(), 'private key list')
|
||||
.go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: Text(s.download),
|
||||
onTap: () =>
|
||||
AppRoute(const SFTPDownloadedPage(), 'snippet list')
|
||||
.go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.snippet_folder),
|
||||
title: Text(s.snippet),
|
||||
onTap: () => AppRoute(const SnippetListPage(), 'snippet list')
|
||||
.go(context),
|
||||
),
|
||||
AboutListTile(
|
||||
icon: const Icon(Icons.text_snippet),
|
||||
applicationName: BuildData.name,
|
||||
applicationVersion: _buildVersionStr(),
|
||||
applicationIcon: _buildIcon(),
|
||||
aboutBoxChildren: [
|
||||
UrlText(
|
||||
text: s.madeWithLove(myGithub), replace: 'LollipopKit'),
|
||||
UrlText(
|
||||
text: s.aboutThanks(rainSunMeGithub),
|
||||
replace: 'RainSunMe',
|
||||
),
|
||||
],
|
||||
child: Text(s.license),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -137,14 +270,28 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 60, maxWidth: 60),
|
||||
child: appIcon,
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 53, maxWidth: 53),
|
||||
child: Container(
|
||||
color: primaryColor,
|
||||
)),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 83, maxWidth: 83),
|
||||
child: appIcon,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _buildVersionStr() {
|
||||
return 'Ver: 1.0.${BuildData.build}';
|
||||
var mod = '';
|
||||
if (BuildData.modifications != 0) {
|
||||
mod = '(+${BuildData.modifications})';
|
||||
}
|
||||
return 'Ver: 1.0.${BuildData.build}$mod';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
174
lib/view/page/ping.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/ping_result.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
class PingPage extends StatefulWidget {
|
||||
const PingPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PingPageState createState() => _PingPageState();
|
||||
}
|
||||
|
||||
class _PingPageState extends State<PingPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late TextEditingController _textEditingController;
|
||||
late MediaQueryData _media;
|
||||
final List<PingResult> _results = [];
|
||||
late S s;
|
||||
static const summaryTextStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController = TextEditingController(text: '');
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 13),
|
||||
buildInput(context, _textEditingController,
|
||||
maxLines: 1, onSubmitted: (_) => doPing()),
|
||||
_buildControl(),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: _media.size.height * 0.6,
|
||||
child: ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: _results.length,
|
||||
itemBuilder: (context, index) {
|
||||
final result = _results[index];
|
||||
return _buildResultItem(result);
|
||||
}),
|
||||
),
|
||||
])),
|
||||
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultItem(PingResult result) {
|
||||
final unknown = s.unknown;
|
||||
final ms = s.ms;
|
||||
return RoundRectCard(ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
|
||||
title: Text(result.serverName,
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold, color: primaryColor)),
|
||||
subtitle: Text(
|
||||
_buildPingSummary(result, unknown, ms),
|
||||
style: summaryTextStyle,
|
||||
),
|
||||
trailing: Text(
|
||||
'${s.pingAvg}${result.statistic?.avg?.toStringAsFixed(2) ?? s.unknown} $ms',
|
||||
style: TextStyle(fontSize: 14, color: primaryColor)),
|
||||
));
|
||||
}
|
||||
|
||||
String _buildPingSummary(PingResult result, String unknown, String ms) {
|
||||
final ip = result.ip ?? unknown;
|
||||
if (result.results == null || result.results!.isEmpty) {
|
||||
return '$ip - ${s.noResult}';
|
||||
}
|
||||
final ttl = result.results?.first.ttl ?? unknown;
|
||||
final loss = result.statistic?.loss ?? unknown;
|
||||
final min = result.statistic?.min ?? unknown;
|
||||
final max = result.statistic?.max ?? unknown;
|
||||
return '$ip\n${s.ttl}: $ttl, ${s.loss}: $loss%\n${s.min}: $min $ms, ${s.max}: $max $ms';
|
||||
}
|
||||
|
||||
Future<void> doPing() async {
|
||||
_results.clear();
|
||||
final target = _textEditingController.text.trim();
|
||||
if (target.isEmpty) {
|
||||
showSnackBar(context, Text(s.pingInputIP));
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait(locator<ServerProvider>().servers.map((e) async {
|
||||
if (e.client == null) {
|
||||
return;
|
||||
}
|
||||
final result = await e.client!.run('ping -c 3 $target').string;
|
||||
_results.add(PingResult.parse(e.info.name, result));
|
||||
setState(() {});
|
||||
}));
|
||||
}
|
||||
|
||||
Widget _buildControl() {
|
||||
return SizedBox(
|
||||
height: 57,
|
||||
child: RoundRectCard(
|
||||
InkWell(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(primaryColor)),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete),
|
||||
const SizedBox(
|
||||
width: 7,
|
||||
),
|
||||
Text(s.clear)
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
_results.clear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(primaryColor)),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.play_arrow),
|
||||
const SizedBox(
|
||||
width: 7,
|
||||
),
|
||||
Text(s.start)
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
try {
|
||||
doPing();
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text('Error: \n$e'));
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/private_key_info.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/input_decoration.dart';
|
||||
|
||||
@@ -25,6 +27,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
|
||||
late PrivateKeyProvider _provider;
|
||||
late Widget loading;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -33,12 +36,19 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
loading = const SizedBox();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Edit'), actions: [
|
||||
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
|
||||
widget.info != null
|
||||
? IconButton(
|
||||
tooltip: s.delete,
|
||||
onPressed: () {
|
||||
_provider.delInfo(widget.info!);
|
||||
Navigator.of(context).pop();
|
||||
@@ -52,7 +62,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
TextField(
|
||||
controller: nameController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: buildDecoration('Name', icon: Icons.info),
|
||||
decoration: buildDecoration(s.name, icon: Icons.info),
|
||||
),
|
||||
TextField(
|
||||
controller: keyController,
|
||||
@@ -61,28 +71,27 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
maxLines: 10,
|
||||
keyboardType: TextInputType.text,
|
||||
enableSuggestions: false,
|
||||
decoration: buildDecoration('Private Key', icon: Icons.vpn_key),
|
||||
decoration: buildDecoration(s.privateKey, icon: Icons.vpn_key),
|
||||
),
|
||||
TextField(
|
||||
controller: pwdController,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: true,
|
||||
decoration: buildDecoration('Password', icon: Icons.password),
|
||||
decoration: buildDecoration(s.pwd, icon: Icons.password),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
||||
loading
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.save),
|
||||
tooltip: s.save,
|
||||
onPressed: () async {
|
||||
final name = nameController.text;
|
||||
final key = keyController.text;
|
||||
final pwd = pwdController.text;
|
||||
if (name.isEmpty || key.isEmpty || pwd.isEmpty) {
|
||||
showSnackBar(
|
||||
context, const Text('Three fields must not be empty.'));
|
||||
if (name.isEmpty || key.isEmpty) {
|
||||
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
@@ -114,6 +123,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Icon(Icons.save),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/res/padding.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/view/page/private_key/edit.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
@@ -15,11 +18,19 @@ class StoredPrivateKeysPage extends StatefulWidget {
|
||||
|
||||
class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
|
||||
final _textStyle = TextStyle(color: primaryColor);
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Private Keys'),
|
||||
title: Text(s.privateKey, style: size18),
|
||||
),
|
||||
body: Consumer<PrivateKeyProvider>(
|
||||
builder: (_, key, __) {
|
||||
@@ -29,27 +40,30 @@ class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
|
||||
itemCount: key.infos.length,
|
||||
itemExtent: 57,
|
||||
itemBuilder: (context, idx) {
|
||||
return RoundRectCard(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
key.infos[idx].id,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
PrivateKeyEditPage(info: key.infos[idx]),
|
||||
'private key edit page')
|
||||
.go(context),
|
||||
child: Text(
|
||||
'Edit',
|
||||
style: _textStyle,
|
||||
))
|
||||
],
|
||||
return RoundRectCard(Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
key.infos[idx].id,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
PrivateKeyEditPage(info: key.infos[idx]),
|
||||
'private key edit page')
|
||||
.go(context),
|
||||
child: Text(
|
||||
s.edit,
|
||||
style: _textStyle,
|
||||
))
|
||||
],
|
||||
),
|
||||
));
|
||||
})
|
||||
: const Center(child: Text('No saved private keys.'));
|
||||
: Center(child: Text(s.noSavedPrivateKey));
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/data/model/server/net_speed.dart';
|
||||
import 'package:toolbox/data/model/server/server.dart';
|
||||
import 'package:toolbox/data/model/server/server_status.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/res/icon/linux_icons.dart';
|
||||
import 'package:toolbox/data/res/padding.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
const style11 = TextStyle(fontSize: 11);
|
||||
@@ -23,11 +27,13 @@ class ServerDetailPage extends StatefulWidget {
|
||||
class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late MediaQueryData _media;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -41,10 +47,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
Widget _buildMainPage(ServerInfo si) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(si.info.name),
|
||||
title: Text(si.info.name, style: size18),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(17),
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
_buildLinuxIcon(si.status.sysVer),
|
||||
SizedBox(height: _media.size.height * 0.03),
|
||||
@@ -68,41 +74,44 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
|
||||
Widget _buildCPUView(ServerStatus ss) {
|
||||
return RoundRectCard(
|
||||
SizedBox(
|
||||
height: 12 * ss.cpu2Status.coresCount + 67,
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
height: _media.size.height * 0.02,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%',
|
||||
style: const TextStyle(fontSize: 27),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildCPUTimePercent(ss.cpu2Status.user, 'user'),
|
||||
SizedBox(
|
||||
width: _media.size.width * 0.03,
|
||||
),
|
||||
_buildCPUTimePercent(ss.cpu2Status.sys, 'sys'),
|
||||
SizedBox(
|
||||
width: _media.size.width * 0.03,
|
||||
),
|
||||
_buildCPUTimePercent(ss.cpu2Status.nice, 'nice'),
|
||||
SizedBox(
|
||||
width: _media.size.width * 0.03,
|
||||
),
|
||||
_buildCPUTimePercent(ss.cpu2Status.idle, 'idle')
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
_buildCPUProgress(ss)
|
||||
]),
|
||||
Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: SizedBox(
|
||||
height: 12 * ss.cpu2Status.coresCount + 67,
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
height: _media.size.height * 0.02,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${ss.cpu2Status.usedPercent(coreIdx: 0).toInt()}%',
|
||||
style: const TextStyle(fontSize: 27),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildCPUTimePercent(ss.cpu2Status.user, 'user'),
|
||||
SizedBox(
|
||||
width: _media.size.width * 0.03,
|
||||
),
|
||||
_buildCPUTimePercent(ss.cpu2Status.sys, 'sys'),
|
||||
SizedBox(
|
||||
width: _media.size.width * 0.03,
|
||||
),
|
||||
_buildCPUTimePercent(ss.cpu2Status.nice, 'nice'),
|
||||
SizedBox(
|
||||
width: _media.size.width * 0.03,
|
||||
),
|
||||
_buildCPUTimePercent(ss.cpu2Status.idle, 'idle')
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
_buildCPUProgress(ss)
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -114,7 +123,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
percent.toStringAsFixed(1) + '%',
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
@@ -158,7 +167,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
|
||||
Widget _buildUpTimeAndSys(ServerStatus ss) {
|
||||
return RoundRectCard(Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -173,65 +182,56 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
));
|
||||
}
|
||||
|
||||
String convertMB(int mb) {
|
||||
const suffix = ['MB', 'GB', 'TB'];
|
||||
double value = mb.toDouble();
|
||||
int squareTimes = 0;
|
||||
for (; value / 1024 > 1 && squareTimes < 3; squareTimes++) {
|
||||
value /= 1024;
|
||||
}
|
||||
var finalValue = value.toStringAsFixed(1);
|
||||
if (finalValue.endsWith('.0')) {
|
||||
finalValue = finalValue.replaceFirst('.0', '');
|
||||
}
|
||||
return '$finalValue ${suffix[squareTimes]}';
|
||||
}
|
||||
|
||||
Widget _buildMemView(ServerStatus ss) {
|
||||
final pColor = primaryColor;
|
||||
final used = ss.memory.used / ss.memory.total;
|
||||
final width = _media.size.width - 17 * 2 - 17 * 2;
|
||||
return RoundRectCard(SizedBox(
|
||||
height: 47,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildMemExplain(convertMB(ss.memory.used), pColor),
|
||||
_buildMemExplain(
|
||||
convertMB(ss.memory.cache), pColor.withAlpha(77)),
|
||||
_buildMemExplain(convertMB(ss.memory.total - ss.memory.used),
|
||||
progressColor.resolve(context))
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 7,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: width * used,
|
||||
const mb = 1024 * 1024;
|
||||
return RoundRectCard(Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: SizedBox(
|
||||
height: 47,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildMemExplain((ss.memory.used * mb).convertBytes, pColor),
|
||||
_buildMemExplain(
|
||||
(ss.memory.cache * mb).convertBytes, pColor.withAlpha(77)),
|
||||
_buildMemExplain(
|
||||
((ss.memory.total - ss.memory.used) * mb).convertBytes,
|
||||
progressColor.resolve(context))
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 7,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: width * used,
|
||||
child: LinearProgressIndicator(
|
||||
value: 1,
|
||||
color: pColor,
|
||||
)),
|
||||
SizedBox(
|
||||
width: width * (1 - used),
|
||||
child: LinearProgressIndicator(
|
||||
value: 1,
|
||||
color: pColor,
|
||||
)),
|
||||
SizedBox(
|
||||
width: width * (1 - used),
|
||||
child: LinearProgressIndicator(
|
||||
// memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
|
||||
value: ss.memory.total == 1
|
||||
? 0
|
||||
: ss.memory.cache / (ss.memory.total - ss.memory.used),
|
||||
backgroundColor: progressColor.resolve(context),
|
||||
color: pColor.withAlpha(77),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
// memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
|
||||
value: ss.memory.total == 1
|
||||
? 0
|
||||
: ss.memory.cache / (ss.memory.total - ss.memory.used),
|
||||
backgroundColor: progressColor.resolve(context),
|
||||
color: pColor.withAlpha(77),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -265,7 +265,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
return RoundRectCard(SizedBox(
|
||||
height: 27 * clone.length + 25,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 17),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: clone.length,
|
||||
itemBuilder: (_, idx) {
|
||||
@@ -300,9 +300,19 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
height: 7,
|
||||
)
|
||||
];
|
||||
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
|
||||
if (ns.devices.isEmpty) {
|
||||
children.add(Center(
|
||||
child: Text(
|
||||
s.noInterface,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||
),
|
||||
));
|
||||
} else {
|
||||
children.addAll(ns.devices.map((e) => _buildNetSpeedItem(ns, e)));
|
||||
}
|
||||
|
||||
return RoundRectCard(Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
|
||||
child: Column(
|
||||
children: children,
|
||||
),
|
||||
|
||||
@@ -8,6 +8,9 @@ import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/private_key/edit.dart';
|
||||
import 'package:toolbox/view/widget/input_decoration.dart';
|
||||
@@ -31,10 +34,12 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
|
||||
late ServerProvider _serverProvider;
|
||||
|
||||
late S s;
|
||||
|
||||
bool usePublicKey = false;
|
||||
|
||||
int _pubKeyIndex = -1;
|
||||
late PrivateKeyInfo _keyInfo;
|
||||
PrivateKeyInfo? _keyInfo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -42,33 +47,35 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
_serverProvider = locator<ServerProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Edit'), actions: [
|
||||
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
|
||||
widget.spi != null
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
showRoundDialog(
|
||||
context,
|
||||
'Attention',
|
||||
Text(
|
||||
'Are you sure to delete server [${widget.spi!.name}]'),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_serverProvider.delServer(widget.spi!);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Yes',
|
||||
style: TextStyle(color: Colors.red),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('No'))
|
||||
]);
|
||||
showRoundDialog(context, s.attention,
|
||||
Text(s.sureToDeleteServer(widget.spi!.name)), [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_serverProvider.delServer(widget.spi!);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel))
|
||||
]);
|
||||
},
|
||||
icon: const Icon(Icons.delete))
|
||||
: const SizedBox()
|
||||
@@ -82,21 +89,21 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
TextField(
|
||||
controller: nameController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration:
|
||||
buildDecoration('Name', icon: Icons.info, hint: 'Example'),
|
||||
decoration: buildDecoration(s.name,
|
||||
icon: Icons.info, hint: s.exampleName),
|
||||
),
|
||||
TextField(
|
||||
controller: ipController,
|
||||
keyboardType: TextInputType.text,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
decoration: buildDecoration('Host',
|
||||
decoration: buildDecoration(s.host,
|
||||
icon: Icons.storage, hint: 'example.com'),
|
||||
),
|
||||
TextField(
|
||||
controller: portController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: buildDecoration('Port',
|
||||
decoration: buildDecoration(s.port,
|
||||
icon: Icons.format_list_numbered, hint: '22'),
|
||||
),
|
||||
TextField(
|
||||
@@ -104,13 +111,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
keyboardType: TextInputType.text,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
decoration: buildDecoration('User',
|
||||
decoration: buildDecoration(s.user,
|
||||
icon: Icons.account_box, hint: 'root'),
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Public Key Auth'),
|
||||
Text(s.keyAuth),
|
||||
Switch(
|
||||
value: usePublicKey,
|
||||
onChanged: (val) => setState(() => usePublicKey = val)),
|
||||
@@ -121,8 +128,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: buildDecoration('Pwd',
|
||||
icon: Icons.password, hint: 'Password'),
|
||||
decoration: buildDecoration(s.pwd,
|
||||
icon: Icons.password, hint: s.pwd),
|
||||
onSubmitted: (_) => {},
|
||||
)
|
||||
: const SizedBox(),
|
||||
@@ -142,7 +149,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
)
|
||||
.toList();
|
||||
tiles.add(ListTile(
|
||||
title: const Text('Add a Private Key'),
|
||||
title: Text(s.addPrivateKey),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
@@ -156,9 +163,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
iconColor: primaryColor,
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
title: const Text(
|
||||
'Choose Key',
|
||||
style: TextStyle(fontSize: 14),
|
||||
title: Text(
|
||||
s.choosePrivateKey,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
children: tiles,
|
||||
);
|
||||
@@ -171,15 +178,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
child: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
if (ipController.text == '') {
|
||||
showSnackBar(context, const Text('Please enter host.'));
|
||||
showSnackBar(context, Text(s.plzEnterHost));
|
||||
return;
|
||||
}
|
||||
if (!usePublicKey && passwordController.text == '') {
|
||||
showSnackBar(context, const Text('Please enter password.'));
|
||||
showSnackBar(context, Text(s.plzEnterPwd));
|
||||
return;
|
||||
}
|
||||
if (usePublicKey && _pubKeyIndex == -1) {
|
||||
showSnackBar(context, const Text('Please select a private key.'));
|
||||
showSnackBar(context, Text(s.plzSelectKey));
|
||||
return;
|
||||
}
|
||||
if (usernameController.text == '') {
|
||||
@@ -188,10 +195,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
if (portController.text == '') {
|
||||
portController.text = '22';
|
||||
}
|
||||
|
||||
if (widget.spi != null && widget.spi!.pubKeyId != null) {
|
||||
_keyInfo ??= locator<PrivateKeyStore>().get(widget.spi!.pubKeyId!);
|
||||
}
|
||||
|
||||
final authorization = usePublicKey
|
||||
? {
|
||||
"privateKey": _keyInfo.privateKey,
|
||||
"passphrase": _keyInfo.password
|
||||
"privateKey": _keyInfo!.privateKey,
|
||||
"passphrase": _keyInfo!.password
|
||||
}
|
||||
: passwordController.text;
|
||||
final spi = ServerPrivateInfo(
|
||||
@@ -200,7 +212,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
port: int.parse(portController.text),
|
||||
user: usernameController.text,
|
||||
authorization: authorization,
|
||||
pubKeyId: usePublicKey ? _keyInfo.id : null);
|
||||
pubKeyId: usePublicKey ? _keyInfo!.id : null);
|
||||
|
||||
if (widget.spi == null) {
|
||||
_serverProvider.addServer(spi);
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/app/menu_item.dart';
|
||||
import 'package:toolbox/data/model/server/server.dart';
|
||||
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/server/server_status.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/apt.dart';
|
||||
import 'package:toolbox/view/page/docker.dart';
|
||||
import 'package:toolbox/view/page/server/detail.dart';
|
||||
import 'package:toolbox/view/page/server/edit.dart';
|
||||
import 'package:toolbox/view/page/sftp/view.dart';
|
||||
import 'package:toolbox/view/page/snippet/list.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
const ServerPage({Key? key}) : super(key: key);
|
||||
@@ -31,6 +40,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
late RefreshController _refreshController;
|
||||
|
||||
late ServerProvider _serverProvider;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -45,6 +55,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
_media = MediaQuery.of(context);
|
||||
_theme = Theme.of(context);
|
||||
_primaryColor = primaryColor;
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,22 +65,27 @@ class _ServerPageState extends State<ServerPage>
|
||||
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
|
||||
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
|
||||
if (pro.servers.isEmpty) {
|
||||
return const Center(
|
||||
return Center(
|
||||
child: Text(
|
||||
'There is no server.\nClick the fab to add one.',
|
||||
s.serverTabEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 13),
|
||||
...pro.servers.map((e) => _buildEachServerCard(e)),
|
||||
SizedBox(height: _media.padding.bottom),
|
||||
],
|
||||
));
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(7),
|
||||
controller: ScrollController(),
|
||||
itemBuilder: (ctx, idx) {
|
||||
if (idx == pro.servers.length) {
|
||||
return SizedBox(height: _media.padding.bottom);
|
||||
}
|
||||
return _buildEachServerCard(pro.servers[idx]);
|
||||
},
|
||||
itemCount: pro.servers.length + 1,
|
||||
separatorBuilder: (_, __) => const SizedBox(
|
||||
height: 3,
|
||||
),
|
||||
);
|
||||
});
|
||||
return Scaffold(
|
||||
body: autoUpdate
|
||||
@@ -86,7 +102,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
onPressed: () =>
|
||||
AppRoute(const ServerEditPage(), 'Add server info page')
|
||||
.go(context),
|
||||
tooltip: 'add a server',
|
||||
tooltip: s.addAServer,
|
||||
heroTag: 'server page fab',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
@@ -94,8 +110,8 @@ class _ServerPageState extends State<ServerPage>
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(ServerInfo si) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
return RoundRectCard(
|
||||
InkWell(
|
||||
onLongPress: () => AppRoute(
|
||||
ServerEditPage(
|
||||
spi: si.info,
|
||||
@@ -103,12 +119,9 @@ class _ServerPageState extends State<ServerPage>
|
||||
'Edit server info page')
|
||||
.go(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: SizedBox(
|
||||
height: _media.size.height * 0.147,
|
||||
child: _buildRealServerCard(
|
||||
si.status, si.info.name, si.connectionState)),
|
||||
),
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: _buildRealServerCard(
|
||||
si.status, si.info.name, si.connectionState, si.info)),
|
||||
onTap: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'),
|
||||
'server detail page')
|
||||
.go(context),
|
||||
@@ -116,8 +129,8 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRealServerCard(
|
||||
ServerStatus ss, String serverName, ServerConnectionState cs) {
|
||||
Widget _buildRealServerCard(ServerStatus ss, String serverName,
|
||||
ServerConnectionState cs, ServerPrivateInfo spi) {
|
||||
final rootDisk =
|
||||
ss.disk.firstWhere((element) => element.mountLocation == '/');
|
||||
|
||||
@@ -131,100 +144,211 @@ class _ServerPageState extends State<ServerPage>
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
serverName,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
hasError
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _media.size.width * 0.57, maxHeight: 17),
|
||||
child: Marquee(
|
||||
accelerationDuration: const Duration(seconds: 3),
|
||||
accelerationCurve: Curves.linear,
|
||||
decelerationDuration: const Duration(seconds: 3),
|
||||
decelerationCurve: Curves.linear,
|
||||
text: topRightStr,
|
||||
textScaleFactor: 1.0,
|
||||
style: style),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
serverName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
size: 17,
|
||||
color: Colors.grey,
|
||||
)
|
||||
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
||||
],
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
hasError
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _media.size.width * 0.57,
|
||||
maxHeight: 15),
|
||||
child: Marquee(
|
||||
accelerationDuration: const Duration(seconds: 3),
|
||||
accelerationCurve: Curves.linear,
|
||||
decelerationDuration: const Duration(seconds: 3),
|
||||
decelerationCurve: Curves.linear,
|
||||
text: topRightStr,
|
||||
textScaleFactor: 1.0,
|
||||
style: style),
|
||||
)
|
||||
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
||||
_buildMoreBtn(spi),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 17,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildPercentCircle(ss.cpu2Status.usedPercent(), 'CPU'),
|
||||
_buildPercentCircle(ss.memory.used / ss.memory.total * 100, 'Mem'),
|
||||
_buildIOData('Net', 'Conn:\n' + ss.tcp.maxConn.toString(),
|
||||
'Fail:\n' + ss.tcp.fail.toString()),
|
||||
_buildIOData('Disk', 'Total:\n' + rootDisk.size,
|
||||
'Used:\n' + rootDisk.usedPercent.toString() + '%')
|
||||
_buildPercentCircle(ss.cpu2Status.usedPercent()),
|
||||
_buildPercentCircle(ss.memory.used / ss.memory.total * 100),
|
||||
_buildIOData('Conn:\n${ss.tcp.maxConn}', 'Fail:\n${ss.tcp.fail}'),
|
||||
_buildIOData(
|
||||
'Total:\n${rootDisk.size}', 'Used:\n${rootDisk.usedPercent}%')
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 13),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildExplainText('CPU'),
|
||||
_buildExplainText('Mem'),
|
||||
_buildExplainText('Net'),
|
||||
_buildExplainText('Disk'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMoreBtn(ServerPrivateInfo spi) {
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton2(
|
||||
customButton: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 1.7),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
size: 17,
|
||||
),
|
||||
),
|
||||
customItemsIndexes: [ServerTabMenuItems.firstItems.length],
|
||||
customItemsHeight: 8,
|
||||
items: [
|
||||
...ServerTabMenuItems.firstItems.map(
|
||||
(item) => DropdownMenuItem<DropdownBtnItem>(
|
||||
value: item,
|
||||
child: item.build,
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem<Divider>(enabled: false, child: Divider()),
|
||||
...ServerTabMenuItems.secondItems.map(
|
||||
(item) => DropdownMenuItem<DropdownBtnItem>(
|
||||
value: item,
|
||||
child: item.build,
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
final item = value as DropdownBtnItem;
|
||||
switch (item) {
|
||||
case ServerTabMenuItems.apt:
|
||||
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
|
||||
break;
|
||||
case ServerTabMenuItems.sftp:
|
||||
AppRoute(SFTPPage(spi), 'SFTP').go(context);
|
||||
break;
|
||||
case ServerTabMenuItems.snippet:
|
||||
AppRoute(
|
||||
SnippetListPage(
|
||||
spi: spi,
|
||||
),
|
||||
'snippet list')
|
||||
.go(context);
|
||||
break;
|
||||
case ServerTabMenuItems.edit:
|
||||
AppRoute(
|
||||
ServerEditPage(
|
||||
spi: spi,
|
||||
),
|
||||
'Edit server info page')
|
||||
.go(context);
|
||||
break;
|
||||
case ServerTabMenuItems.docker:
|
||||
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemHeight: 37,
|
||||
itemPadding: const EdgeInsets.only(left: 17, right: 17),
|
||||
dropdownWidth: 160,
|
||||
dropdownPadding: const EdgeInsets.symmetric(vertical: 7),
|
||||
dropdownDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
dropdownElevation: 8,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExplainText(String text) {
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getTopRightStr(ServerConnectionState cs, String temp, String upTime,
|
||||
String? failedInfo) {
|
||||
switch (cs) {
|
||||
case ServerConnectionState.disconnected:
|
||||
return 'Disconnected';
|
||||
return s.disconnected;
|
||||
case ServerConnectionState.connected:
|
||||
return temp == '' ? (upTime == '' ? 'Loading...' : upTime) : temp;
|
||||
if (temp == '') {
|
||||
if (upTime == '') {
|
||||
return s.serverTabLoading;
|
||||
} else {
|
||||
return upTime;
|
||||
}
|
||||
} else {
|
||||
if (upTime == '') {
|
||||
return temp;
|
||||
} else {
|
||||
return '$temp | $upTime';
|
||||
}
|
||||
}
|
||||
case ServerConnectionState.connecting:
|
||||
return 'Connecting...';
|
||||
return s.serverTabConnecting;
|
||||
case ServerConnectionState.failed:
|
||||
if (failedInfo == null) {
|
||||
return 'Failed';
|
||||
return s.serverTabFailed;
|
||||
}
|
||||
if (failedInfo.contains('encypted')) {
|
||||
return 'Please "save" this private key again.';
|
||||
return s.serverTabPlzSave;
|
||||
}
|
||||
return failedInfo;
|
||||
default:
|
||||
return 'Unknown State';
|
||||
return s.serverTabUnkown;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildIOData(String title, String up, String down) {
|
||||
Widget _buildIOData(String up, String down) {
|
||||
final statusTextStyle = TextStyle(
|
||||
fontSize: 9, color: _theme.textTheme.bodyText1!.color!.withAlpha(177));
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
height: _media.size.height * 0.1,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
up,
|
||||
style: statusTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
down + '\n',
|
||||
style: statusTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
up,
|
||||
style: statusTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
down,
|
||||
style: statusTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
)
|
||||
@@ -233,42 +357,31 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPercentCircle(double percent, String title) {
|
||||
Widget _buildPercentCircle(double percent) {
|
||||
if (percent <= 0) percent = 0.01;
|
||||
if (percent >= 100) percent = 99.9;
|
||||
var size = _media.size.height * 0.15;
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
height: _media.size.height * 0.1,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
child: Stack(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
CircleChart(
|
||||
progressColor: _primaryColor,
|
||||
progressNumber: percent,
|
||||
maxNumber: 100,
|
||||
width: size,
|
||||
height: size / 1.5,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Center(
|
||||
child: CircleChart(
|
||||
progressColor: _primaryColor,
|
||||
progressNumber: percent,
|
||||
maxNumber: 100,
|
||||
width: 53,
|
||||
height: 53,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.0,
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -2,11 +2,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/update.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/res/padding.dart';
|
||||
import 'package:toolbox/data/res/tab.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
@@ -20,15 +25,22 @@ class SettingPage extends StatefulWidget {
|
||||
class _SettingPageState extends State<SettingPage> {
|
||||
late SettingStore _store;
|
||||
late int _selectedColorValue;
|
||||
late int _launchPageIdx;
|
||||
double _intervalValue = 0;
|
||||
late Color priColor;
|
||||
static const textStyle = TextStyle(fontSize: 14);
|
||||
late final ServerProvider _serverProvider;
|
||||
late MediaQueryData _media;
|
||||
late ThemeData _theme;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
priColor = primaryColor;
|
||||
_media = MediaQuery.of(context);
|
||||
_theme = Theme.of(context);
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -36,6 +48,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
super.initState();
|
||||
_serverProvider = locator<ServerProvider>();
|
||||
_store = locator<SettingStore>();
|
||||
_launchPageIdx = _store.launchPage.fetch()!;
|
||||
_intervalValue = _store.serverStatusUpdateInterval.fetch()!.toDouble();
|
||||
}
|
||||
|
||||
@@ -43,14 +56,15 @@ class _SettingPageState extends State<SettingPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Setting'),
|
||||
title: Text(s.setting, style: size18),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(17),
|
||||
children: [
|
||||
_buildAppColorPreview(),
|
||||
_buildUpdateInterval(),
|
||||
_buildCheckUpdate()
|
||||
_buildCheckUpdate(),
|
||||
_buildLaunchPage()
|
||||
].map((e) => RoundRectCard(e)).toList(),
|
||||
),
|
||||
);
|
||||
@@ -61,15 +75,15 @@ class _SettingPageState extends State<SettingPage> {
|
||||
String display;
|
||||
if (app.newestBuild != null) {
|
||||
if (app.newestBuild! > BuildData.build) {
|
||||
display = 'Found: v1.0.${app.newestBuild}, click to update';
|
||||
display = s.versionHaveUpdate(app.newestBuild!);
|
||||
} else {
|
||||
display = 'Current: v1.0.${BuildData.build},is up to date';
|
||||
display = s.versionUpdated(BuildData.build);
|
||||
}
|
||||
} else {
|
||||
display = 'Current: v1.0.${BuildData.build}';
|
||||
display = s.versionUnknownUpdate(BuildData.build);
|
||||
}
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
contentPadding: roundRectCardPadding,
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(
|
||||
display,
|
||||
@@ -82,19 +96,19 @@ class _SettingPageState extends State<SettingPage> {
|
||||
|
||||
Widget _buildUpdateInterval() {
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
tilePadding: roundRectCardPadding,
|
||||
childrenPadding: roundRectCardPadding,
|
||||
textColor: priColor,
|
||||
title: const Text(
|
||||
'Server status update interval',
|
||||
title: Text(
|
||||
s.updateServerStatusInterval,
|
||||
style: textStyle,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Will take effect immediately.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
subtitle: Text(
|
||||
s.willTakEeffectImmediately,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||
),
|
||||
trailing: Text('${_intervalValue.toInt()} s'),
|
||||
trailing: Text('${_intervalValue.toInt()} ${s.second}'),
|
||||
children: [
|
||||
Slider(
|
||||
thumbColor: priColor,
|
||||
@@ -111,16 +125,16 @@ class _SettingPageState extends State<SettingPage> {
|
||||
_store.serverStatusUpdateInterval.put(val.toInt());
|
||||
_serverProvider.startAutoRefresh();
|
||||
},
|
||||
label: '${_intervalValue.toInt()} seconds',
|
||||
label: '${_intervalValue.toInt()} ${s.second}',
|
||||
divisions: 10,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 3,
|
||||
),
|
||||
_intervalValue == 0.0
|
||||
? const Text(
|
||||
'You set to 0, will not update automatically.\nYou can pull to refresh manually.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
? Text(
|
||||
s.updateIntervalEqual0,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
: const SizedBox(),
|
||||
@@ -134,12 +148,8 @@ class _SettingPageState extends State<SettingPage> {
|
||||
Widget _buildAppColorPreview() {
|
||||
return ExpansionTile(
|
||||
textColor: priColor,
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
children: [
|
||||
_buildAppColorPicker(priColor),
|
||||
_buildColorPickerConfirmBtn()
|
||||
],
|
||||
tilePadding: roundRectCardPadding,
|
||||
childrenPadding: roundRectCardPadding,
|
||||
trailing: ClipOval(
|
||||
child: Container(
|
||||
color: priColor,
|
||||
@@ -147,10 +157,14 @@ class _SettingPageState extends State<SettingPage> {
|
||||
width: 27,
|
||||
),
|
||||
),
|
||||
title: const Text(
|
||||
'App primary color',
|
||||
title: Text(
|
||||
s.appPrimaryColor,
|
||||
style: textStyle,
|
||||
));
|
||||
),
|
||||
children: [
|
||||
_buildAppColorPicker(priColor),
|
||||
_buildColorPickerConfirmBtn()
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildAppColorPicker(Color selected) {
|
||||
@@ -171,4 +185,49 @@ class _SettingPageState extends State<SettingPage> {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaunchPage() {
|
||||
return ExpansionTile(
|
||||
textColor: priColor,
|
||||
tilePadding: roundRectCardPadding,
|
||||
childrenPadding: roundRectCardPadding,
|
||||
title: Text(
|
||||
s.launchPage,
|
||||
style: textStyle,
|
||||
),
|
||||
trailing: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: _media.size.width * 0.35),
|
||||
child: Text(
|
||||
tabTitleName(context, _launchPageIdx),
|
||||
style: textStyle,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
children: tabs
|
||||
.map((e) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
tabTitleName(context, tabs.indexOf(e)),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: _theme.textTheme.bodyText2!.color!.withAlpha(177)),
|
||||
),
|
||||
trailing: _buildRadio(tabs.indexOf(e)),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Radio _buildRadio(int index) {
|
||||
return Radio<int>(
|
||||
value: index,
|
||||
groupValue: _launchPageIdx,
|
||||
onChanged: (int? value) {
|
||||
setState(() {
|
||||
_launchPageIdx = value!;
|
||||
_store.launchPage.put(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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_connection_state.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/sftp/absolute_path.dart';
|
||||
import 'package:toolbox/data/model/sftp/download_worker.dart';
|
||||
import 'package:toolbox/data/model/sftp/browser_status.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/sftp_download.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/sftp/downloading.dart';
|
||||
import 'package:toolbox/view/widget/fade_in.dart';
|
||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||
|
||||
class SFTPPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
const SFTPPage(this.spi, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SFTPPageState createState() => _SFTPPageState();
|
||||
}
|
||||
|
||||
class _SFTPPageState extends State<SFTPPage> {
|
||||
final SftpBrowserStatus _status = SftpBrowserStatus();
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
late MediaQueryData _media;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_status.spi = widget.spi;
|
||||
_status.selected = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: (() => showRoundDialog(
|
||||
context,
|
||||
s.choose,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: Text(s.createFolder),
|
||||
onTap: () => mkdir(context)),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
title: Text(s.createFile),
|
||||
onTap: () => newFile(context)),
|
||||
],
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.close))
|
||||
])),
|
||||
icon: const Icon(Icons.add),
|
||||
)
|
||||
]),
|
||||
body: _buildFileView(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get centerCircleLoading => Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _media.size.height * 0.4,
|
||||
),
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildFileView() {
|
||||
if (!_status.selected) {
|
||||
return ListView(
|
||||
children: [
|
||||
_buildDestSelector(),
|
||||
],
|
||||
);
|
||||
}
|
||||
final spi = _status.spi;
|
||||
final si =
|
||||
locator<ServerProvider>().servers.firstWhere((s) => s.info == spi);
|
||||
final client = si.client;
|
||||
if (client == null ||
|
||||
si.connectionState != ServerConnectionState.connected) {
|
||||
return centerCircleLoading;
|
||||
}
|
||||
|
||||
if (_status.files == null) {
|
||||
_status.path = AbsolutePath('/');
|
||||
listDir(path: '/', client: client);
|
||||
return centerCircleLoading;
|
||||
} else {
|
||||
return RefreshIndicator(
|
||||
child: FadeIn(
|
||||
key: Key(_status.spi!.name + _status.path!.path),
|
||||
child: ListView.builder(
|
||||
itemCount: _status.files!.length + 1,
|
||||
controller: _scrollController,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return _buildDestSelector();
|
||||
}
|
||||
final file = _status.files![index - 1];
|
||||
final isDir = file.attr.isDirectory;
|
||||
return ListTile(
|
||||
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
|
||||
title: Text(file.filename),
|
||||
trailing: Text(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
(file.attr.modifyTime ?? 0) * 1000)
|
||||
.toString()
|
||||
.replaceFirst('.000', ''),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
subtitle:
|
||||
isDir ? null : Text((file.attr.size ?? 0).convertBytes),
|
||||
onTap: () {
|
||||
if (isDir) {
|
||||
_status.path?.update(file.filename);
|
||||
listDir(path: _status.path?.path);
|
||||
} else {
|
||||
onItemPress(context, file, true);
|
||||
}
|
||||
},
|
||||
onLongPress: () => onItemPress(context, file, false),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onRefresh: () => listDir(path: _status.path?.path));
|
||||
}
|
||||
}
|
||||
|
||||
void onItemPress(BuildContext context, SftpName file, bool showDownload) {
|
||||
showRoundDialog(
|
||||
context,
|
||||
s.choose,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(s.delete),
|
||||
onTap: () => delete(context, file),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: Text(s.rename),
|
||||
onTap: () => rename(context, file),
|
||||
),
|
||||
showDownload
|
||||
? ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: Text(s.download),
|
||||
onTap: () => download(context, file),
|
||||
)
|
||||
: const SizedBox()
|
||||
],
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel))
|
||||
]);
|
||||
}
|
||||
|
||||
void download(BuildContext context, SftpName name) {
|
||||
showRoundDialog(context, s.download,
|
||||
Text('${s.dl2Local(name.filename)}\n${s.keepForeground}'), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
final prePath = _status.path!.path;
|
||||
final remotePath =
|
||||
prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
|
||||
final local = '${(await sftpDownloadDir).path}$remotePath';
|
||||
final pubKeyId = _status.spi!.pubKeyId;
|
||||
locator<SftpDownloadProvider>().add(
|
||||
DownloadItem(_status.spi!, remotePath, local),
|
||||
key: pubKeyId == null
|
||||
? null
|
||||
: locator<PrivateKeyStore>().get(pubKeyId).privateKey);
|
||||
Navigator.of(context).pop();
|
||||
showRoundDialog(context, s.goSftpDlPage, const SizedBox(), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
|
||||
.go(context);
|
||||
},
|
||||
child: Text(s.ok))
|
||||
]);
|
||||
},
|
||||
child: Text(s.download))
|
||||
]);
|
||||
}
|
||||
|
||||
void delete(BuildContext context, SftpName file) {
|
||||
Navigator.of(context).pop();
|
||||
showRoundDialog(context, s.attention, Text(s.sureDelete(file.filename)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_status.client!.remove(file.filename);
|
||||
Navigator.of(context).pop();
|
||||
listDir();
|
||||
},
|
||||
child: Text(
|
||||
s.delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
void mkdir(BuildContext context) {
|
||||
Navigator.of(context).pop();
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context,
|
||||
s.createFolder,
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: InputDecoration(
|
||||
labelText: s.name,
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(
|
||||
context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.ok)),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
_status.client!
|
||||
.mkdir('${_status.path!.path}/${textController.text}');
|
||||
Navigator.of(context).pop();
|
||||
listDir();
|
||||
},
|
||||
child: Text(
|
||||
s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
void newFile(BuildContext context) {
|
||||
Navigator.of(context).pop();
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context,
|
||||
s.createFile,
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: InputDecoration(
|
||||
labelText: s.name,
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(
|
||||
context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.ok)),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
(await _status.client!
|
||||
.open('${_status.path!.path}/${textController.text}'))
|
||||
.writeBytes(Uint8List(0));
|
||||
Navigator.of(context).pop();
|
||||
listDir();
|
||||
},
|
||||
child: Text(
|
||||
s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
void rename(BuildContext context, SftpName file) {
|
||||
Navigator.of(context).pop();
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context,
|
||||
s.rename,
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: InputDecoration(
|
||||
labelText: s.name,
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(
|
||||
context, s.attention, Text(s.fieldMustNotEmpty), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.ok)),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
await _status.client!
|
||||
.rename(file.filename, textController.text);
|
||||
Navigator.of(context).pop();
|
||||
listDir();
|
||||
},
|
||||
child: Text(
|
||||
s.rename,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> listDir({String? path, SSHClient? client}) async {
|
||||
if (_status.isBusy) {
|
||||
return;
|
||||
}
|
||||
_status.isBusy = true;
|
||||
if (client != null) {
|
||||
final sftpc = await client.sftp();
|
||||
_status.client = sftpc;
|
||||
}
|
||||
try {
|
||||
final fs =
|
||||
await _status.client!.listdir(path ?? (_status.path?.path ?? '/'));
|
||||
fs.sort((a, b) => a.filename.compareTo(b.filename));
|
||||
fs.removeAt(0);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_status.files = fs;
|
||||
_status.isBusy = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
await showRoundDialog(context, s.error, Text(e.toString()), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok))
|
||||
]);
|
||||
if (_status.path!.undo()) {
|
||||
await listDir();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDestSelector() {
|
||||
final str = _status.path?.path;
|
||||
return ExpansionTile(
|
||||
title: Text(_status.spi?.name ?? s.chooseDestination),
|
||||
subtitle: _status.selected
|
||||
? str!.omitStartStr(style: const TextStyle(color: Colors.grey))
|
||||
: null,
|
||||
children: locator<ServerProvider>()
|
||||
.servers
|
||||
.map((e) => _buildDestSelectorItem(e.info))
|
||||
.toList());
|
||||
}
|
||||
|
||||
Widget _buildDestSelectorItem(ServerPrivateInfo spi) {
|
||||
return ListTile(
|
||||
title: Text(spi.name),
|
||||
subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'),
|
||||
onTap: () {
|
||||
_status.spi = spi;
|
||||
_status.selected = true;
|
||||
_status.path = AbsolutePath('/');
|
||||
listDir(
|
||||
client: locator<ServerProvider>()
|
||||
.servers
|
||||
.firstWhere((s) => s.info == spi)
|
||||
.client,
|
||||
path: '/');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/input_decoration.dart';
|
||||
|
||||
@@ -21,6 +23,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
final scriptController = TextEditingController();
|
||||
|
||||
late SnippetProvider _provider;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -28,16 +31,23 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
_provider = locator<SnippetProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Edit'), actions: [
|
||||
appBar: AppBar(title: Text(s.edit, style: size18), actions: [
|
||||
widget.snippet != null
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_provider.delInfo(widget.snippet!);
|
||||
_provider.del(widget.snippet!);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
tooltip: s.delete,
|
||||
icon: const Icon(Icons.delete))
|
||||
: const SizedBox()
|
||||
]),
|
||||
@@ -47,7 +57,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
TextField(
|
||||
controller: nameController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: buildDecoration('Name', icon: Icons.info),
|
||||
decoration: buildDecoration(s.name, icon: Icons.info),
|
||||
),
|
||||
TextField(
|
||||
controller: scriptController,
|
||||
@@ -56,7 +66,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
maxLines: 10,
|
||||
keyboardType: TextInputType.text,
|
||||
enableSuggestions: false,
|
||||
decoration: buildDecoration('Snippet', icon: Icons.code),
|
||||
decoration: buildDecoration(s.snippet, icon: Icons.code),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -66,14 +76,14 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
final name = nameController.text;
|
||||
final script = scriptController.text;
|
||||
if (name.isEmpty || script.isEmpty) {
|
||||
showSnackBar(context, const Text('Two fields must not be empty.'));
|
||||
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
final snippet = Snippet(name, script);
|
||||
if (widget.snippet != null) {
|
||||
_provider.updateInfo(widget.snippet!, snippet);
|
||||
_provider.update(widget.snippet!, snippet);
|
||||
} else {
|
||||
_provider.addInfo(snippet);
|
||||
_provider.add(snippet);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/res/padding.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/snippet/edit.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
class SnippetListPage extends StatefulWidget {
|
||||
const SnippetListPage({Key? key}) : super(key: key);
|
||||
const SnippetListPage({Key? key, this.spi}) : super(key: key);
|
||||
final ServerPrivateInfo? spi;
|
||||
|
||||
@override
|
||||
_SnippetListPageState createState() => _SnippetListPageState();
|
||||
}
|
||||
|
||||
class _SnippetListPageState extends State<SnippetListPage> {
|
||||
int _selectedIndex = 0;
|
||||
late ServerPrivateInfo _selectedIndex;
|
||||
|
||||
final _importFieldController = TextEditingController();
|
||||
final _exportFieldController = TextEditingController();
|
||||
|
||||
final _textStyle = TextStyle(color: primaryColor);
|
||||
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Snippet List'),
|
||||
title: Text(s.snippet, style: size18),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _showImportExport(),
|
||||
tooltip: s.importAndExport,
|
||||
icon: const Icon(Icons.import_export)),
|
||||
],
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@@ -36,6 +60,97 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showImportExport() async {
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.choose,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(s.import),
|
||||
leading: const Icon(Icons.download),
|
||||
onTap: () => _showImportDialog(),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(s.export),
|
||||
leading: const Icon(Icons.file_upload),
|
||||
onTap: () => _showExportDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
[]);
|
||||
}
|
||||
|
||||
Future<void> _showExportDialog() async {
|
||||
Navigator.of(context).pop();
|
||||
_exportFieldController.text = locator<SnippetProvider>().export;
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.export,
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'JSON',
|
||||
),
|
||||
maxLines: 3,
|
||||
controller: _exportFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
child: Text(s.ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _showImportDialog() async {
|
||||
Navigator.of(context).pop();
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.import,
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: s.urlOrJson,
|
||||
),
|
||||
maxLines: 2,
|
||||
controller: _importFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async =>
|
||||
await _import(_importFieldController.text.trim()),
|
||||
child: const Text('GO'),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _import(String text) async {
|
||||
if (text.isEmpty) {
|
||||
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
final snippetProvider = locator<SnippetProvider>();
|
||||
if (text.startsWith('http')) {
|
||||
final resp = await Dio().get(text);
|
||||
if (resp.statusCode != 200) {
|
||||
showSnackBar(
|
||||
context, Text(s.httpFailedWithCode(resp.statusCode ?? '-1')));
|
||||
return;
|
||||
}
|
||||
for (final snippet in getSnippetList(resp.data)) {
|
||||
snippetProvider.add(snippet);
|
||||
}
|
||||
} else {
|
||||
for (final snippet in getSnippetList(text)) {
|
||||
snippetProvider.add(snippet);
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Consumer<SnippetProvider>(
|
||||
builder: (_, key, __) {
|
||||
@@ -45,49 +160,65 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
itemCount: key.snippets.length,
|
||||
itemExtent: 57,
|
||||
itemBuilder: (context, idx) {
|
||||
return RoundRectCard(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
key.snippets[idx].name,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Row(children: [
|
||||
TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
SnippetEditPage(snippet: key.snippets[idx]),
|
||||
'snippet edit page')
|
||||
.go(context),
|
||||
child: Text(
|
||||
'Edit',
|
||||
style: _textStyle,
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => _showRunDialog(key.snippets[idx]),
|
||||
child: Text(
|
||||
'Run',
|
||||
style: _textStyle,
|
||||
))
|
||||
])
|
||||
],
|
||||
return RoundRectCard(Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
key.snippets[idx].name,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Row(children: [
|
||||
TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
SnippetEditPage(
|
||||
snippet: key.snippets[idx]),
|
||||
'snippet edit page')
|
||||
.go(context),
|
||||
child: Text(
|
||||
s.edit,
|
||||
style: _textStyle,
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final snippet = key.snippets[idx];
|
||||
if (widget.spi == null) {
|
||||
_showRunDialog(snippet);
|
||||
return;
|
||||
}
|
||||
run(context, snippet);
|
||||
},
|
||||
child: Text(
|
||||
s.run,
|
||||
style: _textStyle,
|
||||
))
|
||||
])
|
||||
],
|
||||
),
|
||||
));
|
||||
})
|
||||
: const Center(child: Text('No saved snippets.'));
|
||||
: Center(child: Text(s.noSavedSnippet));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showRunDialog(Snippet snippet) {
|
||||
showRoundDialog(context, 'Choose destination',
|
||||
showRoundDialog(context, s.chooseDestination,
|
||||
Consumer<ServerProvider>(builder: (_, provider, __) {
|
||||
if (provider.servers.isEmpty) {
|
||||
return const Text('No server available');
|
||||
return Text(s.noServerAvailable);
|
||||
}
|
||||
_selectedIndex = provider.servers.first.info;
|
||||
return SizedBox(
|
||||
height: 111,
|
||||
child: Stack(children: [
|
||||
Positioned(
|
||||
top: 36,
|
||||
bottom: 36,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 37,
|
||||
decoration: const BoxDecoration(
|
||||
@@ -95,16 +226,13 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
color: Colors.black12,
|
||||
),
|
||||
),
|
||||
top: 36,
|
||||
bottom: 36,
|
||||
left: 0,
|
||||
right: 0,
|
||||
),
|
||||
ListWheelScrollView.useDelegate(
|
||||
itemExtent: 37,
|
||||
diameterRatio: 1.2,
|
||||
controller: FixedExtentScrollController(initialItem: 0),
|
||||
onSelectedItemChanged: (idx) => _selectedIndex = idx,
|
||||
onSelectedItemChanged: (idx) =>
|
||||
_selectedIndex = provider.servers[idx].info,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
childDelegate: ListWheelChildBuilderDelegate(
|
||||
builder: (context, index) => Center(
|
||||
@@ -118,22 +246,21 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
]));
|
||||
}), [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final result = await locator<ServerProvider>()
|
||||
.runSnippet(_selectedIndex, snippet);
|
||||
if (result != null) {
|
||||
showRoundDialog(context, 'Result',
|
||||
Text(result, style: const TextStyle(fontSize: 13)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'))
|
||||
]);
|
||||
}
|
||||
},
|
||||
child: const Text('Run')),
|
||||
onPressed: () async => run(context, snippet), child: Text(s.run)),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel)),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> run(BuildContext context, Snippet snippet) async {
|
||||
final result = await locator<ServerProvider>()
|
||||
.runSnippet(widget.spi ?? _selectedIndex, snippet);
|
||||
if (result != null) {
|
||||
showRoundDialog(context, s.result,
|
||||
Text(result, style: const TextStyle(fontSize: 13)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(), child: Text(s.close))
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
lib/view/widget/center_loading.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const centerLoading = Center(child: CircularProgressIndicator());
|
||||
|
||||
const centerSizedLoading = SizedBox(
|
||||
width: 77,
|
||||
height: 77,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
final loadingIcon = IconButton(onPressed: () {}, icon: centerLoading);
|
||||
44
lib/view/widget/fade_in.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 渐隐渐显实现
|
||||
class FadeIn extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const FadeIn({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MyFadeInState createState() => _MyFadeInState();
|
||||
}
|
||||
|
||||
class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 377),
|
||||
);
|
||||
_animation = Tween(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_controller.forward();
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
lib/view/widget/input_field.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
Widget buildInput(BuildContext context, TextEditingController controller,
|
||||
{int maxLines = 20,
|
||||
String? hint,
|
||||
Function(String)? onSubmitted,
|
||||
bool? obscureText}) {
|
||||
return RoundRectCard(
|
||||
TextField(
|
||||
maxLines: maxLines,
|
||||
onSubmitted: onSubmitted,
|
||||
decoration: InputDecoration(
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
hintText: hint,
|
||||
filled: true,
|
||||
border: InputBorder.none),
|
||||
controller: controller,
|
||||
obscureText: obscureText ?? false,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -8,13 +8,10 @@ class RoundRectCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: child,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 7),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(17))),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
24
lib/view/widget/two_line_text.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TwoLineText extends StatelessWidget {
|
||||
const TwoLineText({Key? key, required this.up, required this.down})
|
||||
: super(key: key);
|
||||
final String up;
|
||||
final String down;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
up,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
Text(
|
||||
down,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,25 @@ const regUrl =
|
||||
class UrlText extends StatelessWidget {
|
||||
final String text;
|
||||
final String? replace;
|
||||
final TextAlign? textAlign;
|
||||
final TextStyle style;
|
||||
|
||||
const UrlText(
|
||||
{Key? key,
|
||||
required this.text,
|
||||
this.replace,
|
||||
this.textAlign,
|
||||
this.style = const TextStyle()})
|
||||
: super(key: key);
|
||||
|
||||
List<InlineSpan> _getTextSpans(bool isDarkMode) {
|
||||
List<InlineSpan> widgets = <InlineSpan>[];
|
||||
final reg = RegExp(regUrl);
|
||||
Iterable<Match> _matches = reg.allMatches(text);
|
||||
Iterable<Match> matches = reg.allMatches(text);
|
||||
List<_ResultMatch> resultMatches = <_ResultMatch>[];
|
||||
int start = 0;
|
||||
|
||||
for (Match match in _matches) {
|
||||
for (Match match in matches) {
|
||||
final group0 = match.group(0);
|
||||
if (group0 != null && group0.isNotEmpty) {
|
||||
if (start != match.start) {
|
||||
@@ -69,6 +71,7 @@ class UrlText extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
text: TextSpan(children: _getTextSpans(isDarkMode(context))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
@@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
|
||||
@@ -6,9 +6,11 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import path_provider_macos
|
||||
import share_plus_macos
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- path_provider_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- share_plus_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||
- share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -15,14 +18,17 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
path_provider_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
|
||||
share_plus_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||
path_provider_macos: 160cab0d5461f0c0e02995469a98f24bdb9a3f1f
|
||||
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
|
||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
|
||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -420,14 +420,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 136;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.79;
|
||||
MARKETING_VERSION = 1.0.136;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -550,14 +550,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 136;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.79;
|
||||
MARKETING_VERSION = 1.0.136;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -574,14 +574,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 136;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.79;
|
||||
MARKETING_VERSION = 1.0.136;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
118
make.dart
@@ -5,6 +5,23 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
const appName = 'ServerBox';
|
||||
const buildDataFilePath = 'lib/data/res/build_data.dart';
|
||||
const apkPath = 'build/app/outputs/flutter-apk/app-release.apk';
|
||||
const xcarchivePath = 'build/ios/archive/Runner.xcarchive';
|
||||
const macosAppPath = 'build/macos/Build/Products/Release/$appName.app';
|
||||
var regiOSProjectVer = RegExp(r'CURRENT_PROJECT_VERSION = .+;');
|
||||
var regiOSMarketVer = RegExp(r'MARKETING_VERSION = .+');
|
||||
const iOSInfoPlistPath = 'ios/Runner.xcodeproj/project.pbxproj';
|
||||
const macOSInfoPlistPath = 'macos/Runner.xcodeproj/project.pbxproj';
|
||||
const skslFileSuffix = '.sksl.json';
|
||||
|
||||
const buildFuncs = {
|
||||
'ios': flutterBuildIOS,
|
||||
'macos': flutterBuildMacOS,
|
||||
'android': flutterBuildAndroid,
|
||||
};
|
||||
|
||||
int? build;
|
||||
|
||||
Future<int> getGitCommitCount() async {
|
||||
final result = await Process.run('git', ['log', '--oneline']);
|
||||
@@ -46,7 +63,7 @@ Future<String> getFlutterVersion() async {
|
||||
Future<Map<String, dynamic>> getBuildData() async {
|
||||
final data = {
|
||||
'name': appName,
|
||||
'build': await getGitCommitCount(),
|
||||
'build': build,
|
||||
'engine': await getFlutterVersion(),
|
||||
'buildAt': DateTime.now().toString(),
|
||||
'modifications': await getGitModificationCount(),
|
||||
@@ -63,27 +80,42 @@ Future<void> updateBuildData() async {
|
||||
print('Updating BuildData...');
|
||||
final data = await getBuildData();
|
||||
print(jsonEncodeWithIndent(data));
|
||||
const path = 'lib/data/res/build_data.dart';
|
||||
await writeStaicConfigFile(data, 'BuildData', path);
|
||||
await writeStaicConfigFile(data, 'BuildData', buildDataFilePath);
|
||||
}
|
||||
|
||||
Future<void> dartFormat() async {
|
||||
final result = await Process.run('dart', ['format', '.']);
|
||||
print('\n${result.stdout}');
|
||||
if (result.exitCode != 0) {
|
||||
print(result.stderr);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void flutterRun(String? mode) {
|
||||
Process.start('flutter', ['run', mode == null ? '' : '--$mode'],
|
||||
Process.start('flutter', mode == null ? ['run'] : ['run', '--$mode'],
|
||||
mode: ProcessStartMode.inheritStdio, runInShell: true);
|
||||
}
|
||||
|
||||
Future<void> flutterBuild(String source, String target, bool isAndroid) async {
|
||||
final startTime = DateTime.now();
|
||||
final build = await getGitCommitCount();
|
||||
|
||||
Future<void> flutterBuild(
|
||||
String source, String target, String buildType) async {
|
||||
final args = [
|
||||
'build',
|
||||
isAndroid ? 'apk' : 'ipa',
|
||||
'--target-platform=android-arm64',
|
||||
'--build-number=$build',
|
||||
'--build-name=1.0.$build'
|
||||
buildType,
|
||||
];
|
||||
if (!isAndroid) args.removeAt(2);
|
||||
// No sksl cache for macos
|
||||
if ('macos' != buildType) {
|
||||
args.add('--bundle-sksl-path=$buildType$skslFileSuffix');
|
||||
}
|
||||
final isAndroid = 'apk' == buildType;
|
||||
// [--target-platform] only for Android
|
||||
if (isAndroid) {
|
||||
args.addAll([
|
||||
'--target-platform=android-arm64',
|
||||
'--build-number=$build',
|
||||
'--build-name=1.0.$build',
|
||||
]);
|
||||
}
|
||||
print('Building with args: ${args.join(' ')}');
|
||||
final buildResult = await Process.run('flutter', args, runInShell: true);
|
||||
final exitCode = buildResult.exitCode;
|
||||
@@ -91,25 +123,49 @@ Future<void> flutterBuild(String source, String target, bool isAndroid) async {
|
||||
if (exitCode == 0) {
|
||||
target = target.replaceFirst('build', build.toString());
|
||||
print('Copying from $source to $target');
|
||||
await File(source).copy(target);
|
||||
print('Done.');
|
||||
if (isAndroid) {
|
||||
await File(source).copy(target);
|
||||
} else {
|
||||
final result = await Process.run('cp', ['-r', source, target]);
|
||||
if (result.exitCode != 0) {
|
||||
print(result.stderr);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
print('Done.\n');
|
||||
} else {
|
||||
print(buildResult.stderr.toString());
|
||||
print('\nBuild failed with exit code $exitCode');
|
||||
exit(exitCode);
|
||||
}
|
||||
final endTime = DateTime.now();
|
||||
print('Spent time: ${endTime.difference(startTime).toString()}');
|
||||
}
|
||||
|
||||
Future<void> flutterBuildIOS() async {
|
||||
await flutterBuild('./build/ios/iphoneos/ToastTiku.app',
|
||||
'./release/${appName}_build.app', false);
|
||||
await changeInfoPlistVersion();
|
||||
await flutterBuild(
|
||||
xcarchivePath, './release/${appName}_ios_build.xcarchive', 'ipa');
|
||||
}
|
||||
|
||||
Future<void> flutterBuildMacOS() async {
|
||||
await changeInfoPlistVersion();
|
||||
await flutterBuild(
|
||||
macosAppPath, './release/${appName}_macos_build.app', 'macos');
|
||||
}
|
||||
|
||||
Future<void> flutterBuildAndroid() async {
|
||||
await flutterBuild('./build/app/outputs/flutter-apk/app-release.apk',
|
||||
'./release/${appName}_build_Arm64.apk', true);
|
||||
await flutterBuild(apkPath, './release/${appName}_build_Arm64.apk', 'apk');
|
||||
}
|
||||
|
||||
Future<void> changeInfoPlistVersion() async {
|
||||
for (final path in [iOSInfoPlistPath, macOSInfoPlistPath]) {
|
||||
final file = File(path);
|
||||
final contents = await file.readAsString();
|
||||
final newContents = contents
|
||||
.replaceAll(regiOSMarketVer, 'MARKETING_VERSION = 1.0.$build;')
|
||||
.replaceAll(regiOSProjectVer, 'CURRENT_PROJECT_VERSION = $build;');
|
||||
await file.writeAsString(newContents);
|
||||
}
|
||||
}
|
||||
|
||||
void main(List<String> args) async {
|
||||
@@ -124,15 +180,23 @@ void main(List<String> args) async {
|
||||
case 'run':
|
||||
return flutterRun(args.length == 2 ? args[1] : null);
|
||||
case 'build':
|
||||
final stopwatch = Stopwatch()..start();
|
||||
build = await getGitCommitCount();
|
||||
await updateBuildData();
|
||||
await dartFormat();
|
||||
if (args.length > 1) {
|
||||
await updateBuildData();
|
||||
if (args[1] == 'android' || args[1] == 'harmony') {
|
||||
return flutterBuildAndroid();
|
||||
} else if (args[1] == 'ios') {
|
||||
return flutterBuildIOS();
|
||||
final platform = args[1];
|
||||
if (buildFuncs.keys.contains(platform)) {
|
||||
await buildFuncs[platform]!();
|
||||
} else {
|
||||
print('Unknown platform: $platform');
|
||||
}
|
||||
print('unkonwn build arg: ${args[1]}');
|
||||
return;
|
||||
}
|
||||
for (final func in buildFuncs.values) {
|
||||
await func();
|
||||
}
|
||||
print('Build finished in ${stopwatch.elapsed}');
|
||||
return;
|
||||
default:
|
||||
print('Unsupported command: $command');
|
||||
|
||||
201
pubspec.lock
@@ -14,21 +14,21 @@ packages:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.8"
|
||||
version: "3.3.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.1.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -62,7 +62,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "36a46aaa41690aac96fa808a6e75841464007a3b"
|
||||
resolved-ref: "01eb9bcc7f1a1690381caeedb476ea98c5295d55"
|
||||
url: "https://github.com/LollipopKit/circle_chart"
|
||||
source: git
|
||||
version: "0.0.3"
|
||||
@@ -86,7 +86,7 @@ packages:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -99,17 +99,17 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: dcb1f6084242a7b4b8e4edd4b180a42ad7f595ca
|
||||
resolved-ref: "585759048345ab7defdfd9cae3a6c76dc451fe51"
|
||||
url: "https://github.com/Countly/countly-sdk-flutter-bridge.git"
|
||||
source: git
|
||||
version: "20.11.4"
|
||||
version: "21.11.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
dart_console:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -123,28 +123,42 @@ packages:
|
||||
name: dartssh2
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0-pre"
|
||||
version: "2.6.1"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
version: "4.0.6"
|
||||
dropdown_button2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dropdown_button2
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
easy_isolate:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_isolate
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
extended_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
version: "6.2.0"
|
||||
extended_image_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: extended_image_library
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.3.0"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -158,7 +172,7 @@ packages:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -178,13 +192,25 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_advanced_drawer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_advanced_drawer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.1"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_material_color_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -198,14 +224,14 @@ packages:
|
||||
name: flutter_native_splash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
version: "2.1.6"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.6"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -229,7 +255,7 @@ packages:
|
||||
name: hive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.1.0"
|
||||
hive_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -264,21 +290,35 @@ packages:
|
||||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.3"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
version: "0.6.4"
|
||||
lint:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lint
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "2.0.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -292,7 +332,7 @@ packages:
|
||||
name: marquee
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -300,6 +340,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -307,6 +354,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -320,56 +374,56 @@ packages:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
version: "1.8.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
version: "2.0.10"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
version: "2.0.14"
|
||||
path_provider_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.9"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.6"
|
||||
path_provider_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
version: "2.0.6"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.4"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
version: "2.0.6"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -383,14 +437,14 @@ packages:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
version: "5.0.0"
|
||||
pinenacl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pinenacl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.3"
|
||||
version: "0.3.4"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -411,7 +465,7 @@ packages:
|
||||
name: pointycastle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
version: "3.6.0"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -439,7 +493,49 @@ packages:
|
||||
name: r_upgrade
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.7+2"
|
||||
version: "0.3.8+1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
share_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
share_plus_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
share_plus_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
share_plus_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -451,7 +547,7 @@ packages:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
version: "1.8.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -486,7 +582,7 @@ packages:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.4.9"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -507,35 +603,35 @@ packages:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.17"
|
||||
version: "6.1.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.13"
|
||||
version: "6.0.17"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.13"
|
||||
version: "6.0.16"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "3.0.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "3.0.1"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -549,49 +645,42 @@ packages:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.11"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
validators:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: validators
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.5.2"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.2.0+1"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.3.1"
|
||||
version: "5.4.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -600,5 +689,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=2.15.1 <3.0.0"
|
||||
flutter: ">=2.5.0"
|
||||
dart: ">=2.17.0 <3.0.0"
|
||||
flutter: ">=3.0.0"
|
||||
|
||||
46
pubspec.yaml
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@@ -29,34 +29,41 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
provider: ^6.0.0
|
||||
get_it: ^7.2.0
|
||||
hive: ^2.0.0
|
||||
hive_flutter: ^1.0.0
|
||||
dio: ^4.0.0
|
||||
after_layout: ^1.1.0
|
||||
extended_image: ^5.1.3
|
||||
extended_image: ^6.0.3
|
||||
url_launcher: ^6.0.9
|
||||
countly_flutter:
|
||||
git:
|
||||
git:
|
||||
url: https://github.com/Countly/countly-sdk-flutter-bridge.git
|
||||
ref: master
|
||||
dartssh2: ^2.3.0-pre
|
||||
dartssh2: ^2.3.1-pre
|
||||
logging: ^1.0.2
|
||||
flutter_material_color_picker: ^1.1.0+2
|
||||
circle_chart:
|
||||
git:
|
||||
circle_chart:
|
||||
git:
|
||||
url: https://github.com/LollipopKit/circle_chart
|
||||
ref: main
|
||||
# path: ../circle_chart
|
||||
clipboard: ^0.1.3
|
||||
r_upgrade: ^0.3.6
|
||||
pull_to_refresh: ^2.0.0
|
||||
marquee: ^2.2.0
|
||||
|
||||
dropdown_button2: ^1.1.1
|
||||
flutter_advanced_drawer: ^1.3.0
|
||||
path_provider: ^2.0.9
|
||||
easy_isolate: ^1.3.0
|
||||
share_plus: ^4.0.4
|
||||
intl: ^0.17.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_native_splash: ^1.2.4
|
||||
flutter_native_splash: ^2.1.6
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -65,11 +72,10 @@ dev_dependencies:
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^1.0.0
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
|
||||
@@ -90,13 +96,10 @@ flutter:
|
||||
- assets/linux/opensuse.png
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
@@ -116,7 +119,6 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
flutter_native_splash:
|
||||
# This package generates native code to customize Flutter's default white native splash screen
|
||||
# with background color and splash image.
|
||||
@@ -124,17 +126,14 @@ flutter_native_splash:
|
||||
# flutter pub run flutter_native_splash:create
|
||||
# To restore Flutter's default white splash screen, run the following command in the terminal:
|
||||
# flutter pub run flutter_native_splash:remove
|
||||
|
||||
# color or background_image is the only required parameter. Use color to set the background
|
||||
# of your splash screen to a solid color. Use background_image to set the background of your
|
||||
# splash screen to a png image. This is useful for gradients. The image will be stretch to the
|
||||
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
|
||||
color: "#ffffff"
|
||||
#background_image: "assets/background.png"
|
||||
|
||||
# Optional parameters are listed below. To enable a parameter, uncomment the line by removing
|
||||
# the leading # character.
|
||||
|
||||
# The image parameter allows you to specify an image used in the splash screen. It must be a
|
||||
# png file and should be sized for 4x pixel density.
|
||||
image: assets/app_icon.png
|
||||
@@ -144,16 +143,14 @@ flutter_native_splash:
|
||||
# parameters from above. If the image_dark parameter is specified, color_dark or
|
||||
# background_image_dark must be specified. color_dark and background_image_dark cannot both be
|
||||
# set.
|
||||
#color_dark: "#042a49"
|
||||
color_dark: "#121212"
|
||||
#background_image_dark: "assets/dark-background.png"
|
||||
#image_dark: assets/splash-invert.png
|
||||
|
||||
# The android, ios and web parameters can be used to disable generating a splash screen on a given
|
||||
# platform.
|
||||
#android: false
|
||||
#ios: false
|
||||
#web: false
|
||||
|
||||
# The position of the splash image can be set with android_gravity, ios_content_mode, and
|
||||
# web_image_mode parameters. All default to center.
|
||||
#
|
||||
@@ -171,7 +168,6 @@ flutter_native_splash:
|
||||
#
|
||||
# web_image_mode can be one of the following modes: center, contain, stretch, and cover.
|
||||
#web_image_mode: center
|
||||
|
||||
# To hide the notification bar, use the fullscreen parameter. Has no affect in web since web
|
||||
# has no notification bar. Defaults to false.
|
||||
# NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads.
|
||||
@@ -179,13 +175,13 @@ flutter_native_splash:
|
||||
# WidgetsFlutterBinding.ensureInitialized();
|
||||
# SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||
#fullscreen: true
|
||||
|
||||
# If you have changed the name(s) of your info.plist file(s), you can specify the filename(s)
|
||||
# with the info_plist_files parameter. Remove only the # characters in the three lines below,
|
||||
# do not remove any spaces:
|
||||
#info_plist_files:
|
||||
# - 'ios/Runner/Info-Debug.plist'
|
||||
# - 'ios/Runner/Info-Release.plist'
|
||||
|
||||
# To enable support for Android 12, set the following parameter to true. Defaults to false.
|
||||
#android12: true
|
||||
#android12: true
|
||||
flutter_intl:
|
||||
enabled: true
|
||||
|
||||
|
Before Width: | Height: | Size: 280 KiB |