mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 20:55:25 +01:00
Compare commits
29 Commits
v1.0.1276
...
lollipopki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a0928e2f6 | ||
|
|
61f161d8a6 | ||
|
|
52c80795f4 | ||
|
|
09f1ab2cf2 | ||
|
|
2eeb55c1d8 | ||
|
|
6738ac94f8 | ||
|
|
827d40b8b5 | ||
|
|
928f2becf1 | ||
|
|
7d30af44d6 | ||
|
|
35349a90eb | ||
|
|
8be9b9b10b | ||
|
|
c51cf62015 | ||
|
|
8589b3b4d7 | ||
|
|
7693e30cbf | ||
|
|
874d28be12 | ||
|
|
06070c29b9 | ||
|
|
bb0ada12e6 | ||
|
|
9ceeaf7cc4 | ||
|
|
29a57ad742 | ||
|
|
2c495a44c3 | ||
|
|
cc300c141a | ||
|
|
26efb8e185 | ||
|
|
06ed38ff45 | ||
|
|
7c35abe30e | ||
|
|
78ef181d4a | ||
|
|
3f15caeaf2 | ||
|
|
6458e736fa | ||
|
|
99fda8b747 | ||
|
|
c5cbb12ac3 |
2
.github/workflows/analysis.yml
vendored
2
.github/workflows/analysis.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -17,10 +17,10 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
releaseAndroid:
|
releaseAndroid:
|
||||||
name: Release android
|
name: Release android
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -53,10 +53,10 @@ jobs:
|
|||||||
|
|
||||||
releaseLinux:
|
releaseLinux:
|
||||||
name: Release linux
|
name: Release linux
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
# runs-on: macos-latest
|
# runs-on: macos-latest
|
||||||
# steps:
|
# steps:
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v6
|
||||||
# - name: Install Flutter
|
# - name: Install Flutter
|
||||||
# uses: subosito/flutter-action@v2
|
# uses: subosito/flutter-action@v2
|
||||||
# - name: Build
|
# - name: Build
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.6.0' apply false
|
id "com.android.application" version '8.9.1' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,19 +88,19 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||||
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741
|
||||||
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
|
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
|
||||||
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
|
plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
||||||
|
|
||||||
|
|||||||
@@ -748,7 +748,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -758,7 +758,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -884,7 +884,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -894,7 +894,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -912,7 +912,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -922,7 +922,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -943,7 +943,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -956,7 +956,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
@@ -982,7 +982,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1018,7 +1018,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1031,7 +1031,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1054,7 +1054,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1066,7 +1066,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
@@ -1095,7 +1095,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1107,7 +1107,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
PRODUCT_NAME = ServerBox;
|
PRODUCT_NAME = ServerBox;
|
||||||
@@ -1133,7 +1133,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1145,7 +1145,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
PRODUCT_NAME = ServerBox;
|
PRODUCT_NAME = ServerBox;
|
||||||
|
|||||||
23
lib/app.dart
23
lib/app.dart
@@ -25,6 +25,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_setup(context);
|
_setup(context);
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: RNodes.app,
|
listenable: RNodes.app,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
@@ -39,6 +40,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
|
|
||||||
Widget _build(BuildContext context) {
|
Widget _build(BuildContext context) {
|
||||||
final colorSeed = Color(Stores.setting.colorSeed.fetch());
|
final colorSeed = Color(Stores.setting.colorSeed.fetch());
|
||||||
|
|
||||||
UIs.colorSeed = colorSeed;
|
UIs.colorSeed = colorSeed;
|
||||||
UIs.primaryColor = colorSeed;
|
UIs.primaryColor = colorSeed;
|
||||||
|
|
||||||
@@ -61,14 +63,31 @@ class _MyAppState extends State<MyApp> {
|
|||||||
Widget _buildDynamicColor(BuildContext context) {
|
Widget _buildDynamicColor(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (light, dark) {
|
builder: (light, dark) {
|
||||||
final lightTheme = ThemeData(useMaterial3: true, colorScheme: light);
|
final lightSeed = light?.primary;
|
||||||
final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
|
final darkSeed = dark?.primary;
|
||||||
|
|
||||||
|
final lightTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorSchemeSeed: lightSeed,
|
||||||
|
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||||
|
);
|
||||||
|
final darkTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorSchemeSeed: darkSeed,
|
||||||
|
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
if (context.isDark && dark != null) {
|
if (context.isDark && dark != null) {
|
||||||
UIs.primaryColor = dark.primary;
|
UIs.primaryColor = dark.primary;
|
||||||
UIs.colorSeed = dark.primary;
|
UIs.colorSeed = dark.primary;
|
||||||
} else if (!context.isDark && light != null) {
|
} else if (!context.isDark && light != null) {
|
||||||
UIs.primaryColor = light.primary;
|
UIs.primaryColor = light.primary;
|
||||||
UIs.colorSeed = light.primary;
|
UIs.colorSeed = light.primary;
|
||||||
|
} else {
|
||||||
|
final fallbackColor = Color(Stores.setting.colorSeed.fetch());
|
||||||
|
UIs.primaryColor = fallbackColor;
|
||||||
|
UIs.colorSeed = fallbackColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildApp(context, light: lightTheme, dark: darkTheme);
|
return _buildApp(context, light: lightTheme, dark: darkTheme);
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Updating Android sessions: $payload');
|
Loggers.app.info('Updating Android sessions: $payload');
|
||||||
await _channel.invokeMethod('updateSessions', payload);
|
await _channel.invokeMethod('updateSessions', payload);
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
// ignore
|
Loggers.app.warning('Failed to update Android sessions', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,8 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
final res = await _channel.invokeMethod('isServiceRunning');
|
final res = await _channel.invokeMethod('isServiceRunning');
|
||||||
return res == true;
|
return res == true;
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to check if Android service is running', e, s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,9 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Starting iOS Live Activity: $payload');
|
Loggers.app.info('Starting iOS Live Activity: $payload');
|
||||||
await _channel.invokeMethod('startLiveActivity', payload);
|
await _channel.invokeMethod('startLiveActivity', payload);
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to start iOS Live Activity', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> updateLiveActivity(String payload) async {
|
static Future<void> updateLiveActivity(String payload) async {
|
||||||
@@ -65,7 +68,9 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Updating iOS Live Activity: $payload');
|
Loggers.app.info('Updating iOS Live Activity: $payload');
|
||||||
await _channel.invokeMethod('updateLiveActivity', payload);
|
await _channel.invokeMethod('updateLiveActivity', payload);
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to update iOS Live Activity', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> stopLiveActivity() async {
|
static Future<void> stopLiveActivity() async {
|
||||||
@@ -73,7 +78,9 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Stopping iOS Live Activity');
|
Loggers.app.info('Stopping iOS Live Activity');
|
||||||
await _channel.invokeMethod('stopLiveActivity');
|
await _channel.invokeMethod('stopLiveActivity');
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to stop iOS Live Activity', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a handler for native -> Flutter callbacks.
|
/// Register a handler for native -> Flutter callbacks.
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ class SshDiscoveryService {
|
|||||||
// Some tools return non-zero but still have useful output
|
// Some tools return non-zero but still have useful output
|
||||||
if (out.trim().isNotEmpty) return out;
|
if (out.trim().isNotEmpty) return out;
|
||||||
return null;
|
return null;
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to run command: $exe ${args.join(' ')}', e, s);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ class SshDiscoveryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matchCount == 0) {
|
if (matchCount == 0) {
|
||||||
lprint(
|
Loggers.app.warning(
|
||||||
'[ssh_discovery] Warning: No ARP entries parsed on macOS. Output may be unexpected or localized. Output sample: ${s.length > 100 ? '${s.substring(0, 100)}...' : s}',
|
'[ssh_discovery] Warning: No ARP entries parsed on macOS. Output may be unexpected or localized. Output sample: ${s.length > 100 ? '${s.substring(0, 100)}...' : s}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -176,8 +177,7 @@ class SshDiscoveryService {
|
|||||||
r'inet\s+(\d+\.\d+\.\d+\.\d+)\s+netmask\s+0x([0-9a-fA-F]+)(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))?',
|
r'inet\s+(\d+\.\d+\.\d+\.\d+)\s+netmask\s+0x([0-9a-fA-F]+)(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))?',
|
||||||
).firstMatch(line);
|
).firstMatch(line);
|
||||||
if (ipm == null) {
|
if (ipm == null) {
|
||||||
// Log unexpected format but continue processing other lines
|
Loggers.app.warning('[ssh_discovery] Warning: Unexpected ifconfig line format: $line');
|
||||||
lprint('[ssh_discovery] Warning: Unexpected ifconfig line format: $line');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final ip = InternetAddress(ipm.group(1)!);
|
final ip = InternetAddress(ipm.group(1)!);
|
||||||
@@ -190,7 +190,7 @@ class SshDiscoveryService {
|
|||||||
final brd = InternetAddress(ipm.group(3) ?? _broadcastAddress(ip, mask).address);
|
final brd = InternetAddress(ipm.group(3) ?? _broadcastAddress(ip, mask).address);
|
||||||
res.add(_Cidr(ip, prefix, mask, net, brd));
|
res.add(_Cidr(ip, prefix, mask, net, brd));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lprint('[ssh_discovery] Error parsing ifconfig output: $e, line: $line');
|
Loggers.app.warning('[ssh_discovery] Error parsing ifconfig output: $e, line: $line');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,9 @@ class SshDiscoveryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to discover mDNS SSH candidates on macOS', e, s);
|
||||||
|
}
|
||||||
} else if (_isLinux) {
|
} else if (_isLinux) {
|
||||||
final s = await _run('/usr/bin/avahi-browse', ['-rat', '_ssh._tcp']);
|
final s = await _run('/usr/bin/avahi-browse', ['-rat', '_ssh._tcp']);
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
@@ -335,7 +337,8 @@ class _Scanner {
|
|||||||
);
|
);
|
||||||
final banner = await c.future.timeout(timeout, onTimeout: () => null);
|
final banner = await c.future.timeout(timeout, onTimeout: () => null);
|
||||||
return _ScanResult(ip, banner);
|
return _ScanResult(ip, banner);
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to probe SSH at ${ip.address}', e, s);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
sub?.cancel();
|
sub?.cancel();
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ final class BakSyncer extends SyncIface {
|
|||||||
return MergeableUtils.fromJsonString(content, pwd).$1;
|
return MergeableUtils.fromJsonString(content, pwd).$1;
|
||||||
}
|
}
|
||||||
return MergeableUtils.fromJsonString(content).$1;
|
return MergeableUtils.fromJsonString(content).$1;
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to parse backup file with password, trying without password', e, s);
|
||||||
// Fallback: try without password if detection failed
|
// Fallback: try without password if detection failed
|
||||||
return MergeableUtils.fromJsonString(content).$1;
|
return MergeableUtils.fromJsonString(content).$1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,77 @@ String getPrivateKey(String id) {
|
|||||||
return pki.key;
|
return pki.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Spi> resolveMergedJumpChain(
|
||||||
|
Spi target, {
|
||||||
|
List<Spi>? jumpChain,
|
||||||
|
}) {
|
||||||
|
final injectedSpiMap = <String, Spi>{};
|
||||||
|
if (jumpChain != null) {
|
||||||
|
for (final s in jumpChain) {
|
||||||
|
injectedSpiMap[s.id] = s;
|
||||||
|
if (s.oldId.isNotEmpty) {
|
||||||
|
injectedSpiMap[s.oldId] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spi resolveSpi(String id) {
|
||||||
|
final injected = injectedSpiMap[id];
|
||||||
|
if (injected != null) return injected;
|
||||||
|
if (jumpChain != null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
|
||||||
|
}
|
||||||
|
final fromStore = Stores.server.box.get(id);
|
||||||
|
if (fromStore == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
|
||||||
|
}
|
||||||
|
return fromStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _resolveMergedJumpChainInternal(target, resolveSpi: resolveSpi);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Spi> _resolveMergedJumpChainInternal(
|
||||||
|
Spi target, {
|
||||||
|
required Spi Function(String id) resolveSpi,
|
||||||
|
}) {
|
||||||
|
final roots = target.jumpChainIds ?? (target.jumpId == null ? const <String>[] : [target.jumpId!]);
|
||||||
|
if (roots.isEmpty) return const <Spi>[];
|
||||||
|
|
||||||
|
final seen = <String>{};
|
||||||
|
final stack = <String>{};
|
||||||
|
final out = <Spi>[];
|
||||||
|
|
||||||
|
String normId(Spi spi) => spi.id.isNotEmpty ? spi.id : spi.oldId;
|
||||||
|
|
||||||
|
void dfs(String id) {
|
||||||
|
final hop = resolveSpi(id);
|
||||||
|
final norm = normId(hop);
|
||||||
|
|
||||||
|
if (stack.contains(norm)) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at $norm');
|
||||||
|
}
|
||||||
|
if (seen.contains(norm)) return;
|
||||||
|
|
||||||
|
stack.add(norm);
|
||||||
|
final deps = hop.jumpChainIds ?? (hop.jumpId == null ? const <String>[] : [hop.jumpId!]);
|
||||||
|
for (final dep in deps) {
|
||||||
|
dfs(dep);
|
||||||
|
}
|
||||||
|
stack.remove(norm);
|
||||||
|
|
||||||
|
if (seen.add(norm)) {
|
||||||
|
out.add(hop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final r in roots) {
|
||||||
|
dfs(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
Future<SSHClient> genClient(
|
Future<SSHClient> genClient(
|
||||||
Spi spi, {
|
Spi spi, {
|
||||||
void Function(GenSSHClientStatus)? onStatus,
|
void Function(GenSSHClientStatus)? onStatus,
|
||||||
@@ -45,14 +116,17 @@ Future<SSHClient> genClient(
|
|||||||
/// Only pass this param if using multi-threading and key login
|
/// Only pass this param if using multi-threading and key login
|
||||||
String? privateKey,
|
String? privateKey,
|
||||||
|
|
||||||
/// Only pass this param if using multi-threading and key login
|
/// Pre-resolved jump chain (in `spi.jumpId` order: immediate -> farthest).
|
||||||
String? jumpPrivateKey,
|
|
||||||
Duration timeout = const Duration(seconds: 5),
|
|
||||||
|
|
||||||
/// [Spi] of the jump server
|
|
||||||
///
|
///
|
||||||
/// Must pass this param if using multi-threading and key login
|
/// This is mainly used when `Stores` is unavailable (e.g. in an isolate).
|
||||||
Spi? jumpSpi,
|
List<Spi>? jumpChain,
|
||||||
|
|
||||||
|
/// Private keys for [jumpChain], aligned by index.
|
||||||
|
///
|
||||||
|
/// If a jump server uses key auth (`keyId != null`), you must provide the
|
||||||
|
/// decrypted key pem here (or `genClient` will try to read from `Stores`).
|
||||||
|
List<String?>? jumpPrivateKeys,
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
|
||||||
/// Handle keyboard-interactive authentication
|
/// Handle keyboard-interactive authentication
|
||||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
@@ -60,6 +134,41 @@ Future<SSHClient> genClient(
|
|||||||
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
||||||
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
||||||
}) async {
|
}) async {
|
||||||
|
return _genClientInternal(
|
||||||
|
spi,
|
||||||
|
onStatus: onStatus,
|
||||||
|
privateKey: privateKey,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
knownHostFingerprints: knownHostFingerprints,
|
||||||
|
onHostKeyAccepted: onHostKeyAccepted,
|
||||||
|
onHostKeyPrompt: onHostKeyPrompt,
|
||||||
|
visited: <String>{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SSHClient> _genClientInternal(
|
||||||
|
Spi spi, {
|
||||||
|
void Function(GenSSHClientStatus)? onStatus,
|
||||||
|
String? privateKey,
|
||||||
|
List<Spi>? jumpChain,
|
||||||
|
List<String?>? jumpPrivateKeys,
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
|
Map<String, String>? knownHostFingerprints,
|
||||||
|
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
||||||
|
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
||||||
|
required Set<String> visited,
|
||||||
|
SSHSocket? socketOverride,
|
||||||
|
bool followJumpConfig = true,
|
||||||
|
}) async {
|
||||||
|
final identifier = _hostIdentifier(spi);
|
||||||
|
if (!visited.add(identifier)) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at ${spi.name} ($identifier)');
|
||||||
|
}
|
||||||
|
|
||||||
onStatus?.call(GenSSHClientStatus.socket);
|
onStatus?.call(GenSSHClientStatus.socket);
|
||||||
|
|
||||||
final hostKeyCache = Map<String, String>.from(knownHostFingerprints ?? _loadKnownHostFingerprints());
|
final hostKeyCache = Map<String, String>.from(knownHostFingerprints ?? _loadKnownHostFingerprints());
|
||||||
@@ -68,37 +177,126 @@ Future<SSHClient> genClient(
|
|||||||
|
|
||||||
String? alterUser;
|
String? alterUser;
|
||||||
|
|
||||||
final socket = await () async {
|
final (socket, hopClients) = await () async {
|
||||||
// Proxy
|
if (socketOverride != null) return (socketOverride, <SSHClient>[]);
|
||||||
final jumpSpi_ = () {
|
|
||||||
// Multi-thread or key login
|
|
||||||
if (jumpSpi != null) return jumpSpi;
|
|
||||||
// Main thread
|
|
||||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
|
||||||
}();
|
|
||||||
if (jumpSpi_ != null) {
|
|
||||||
final jumpClient = await genClient(
|
|
||||||
jumpSpi_,
|
|
||||||
privateKey: jumpPrivateKey,
|
|
||||||
timeout: timeout,
|
|
||||||
knownHostFingerprints: hostKeyCache,
|
|
||||||
onHostKeyAccepted: hostKeyPersist,
|
|
||||||
onHostKeyPrompt: onHostKeyPrompt,
|
|
||||||
);
|
|
||||||
|
|
||||||
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
if (followJumpConfig) {
|
||||||
|
final injectedSpiMap = <String, Spi>{};
|
||||||
|
final injectedKeyMap = <String, String?>{};
|
||||||
|
|
||||||
|
if (jumpChain != null) {
|
||||||
|
for (var i = 0; i < jumpChain.length; i++) {
|
||||||
|
final s = jumpChain[i];
|
||||||
|
injectedSpiMap[s.id] = s;
|
||||||
|
if (s.oldId.isNotEmpty) injectedSpiMap[s.oldId] = s;
|
||||||
|
if (jumpPrivateKeys != null && i < jumpPrivateKeys.length) {
|
||||||
|
injectedKeyMap[s.id] = jumpPrivateKeys[i];
|
||||||
|
if (s.oldId.isNotEmpty) injectedKeyMap[s.oldId] = jumpPrivateKeys[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spi resolveSpi(String id) {
|
||||||
|
final injected = injectedSpiMap[id];
|
||||||
|
if (injected != null) return injected;
|
||||||
|
if (jumpChain != null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
|
||||||
|
}
|
||||||
|
final fromStore = Stores.server.box.get(id);
|
||||||
|
if (fromStore == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
|
||||||
|
}
|
||||||
|
return fromStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? resolveHopPrivateKey(Spi hop) {
|
||||||
|
final keyId = hop.keyId;
|
||||||
|
if (keyId == null) return null;
|
||||||
|
final injected = injectedKeyMap[hop.id] ?? injectedKeyMap[hop.oldId];
|
||||||
|
return injected ?? getPrivateKey(keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hops = _resolveMergedJumpChainInternal(spi, resolveSpi: resolveSpi);
|
||||||
|
if (hops.isNotEmpty) {
|
||||||
|
// Build multi-hop forward chain with dedup/merge.
|
||||||
|
final createdClients = <SSHClient>[];
|
||||||
|
SSHClient? currentClient;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final firstHop = hops.first;
|
||||||
|
final firstKey = resolveHopPrivateKey(firstHop);
|
||||||
|
if (firstHop.keyId != null && firstKey == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(firstHop.keyId ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
currentClient = await _genClientInternal(
|
||||||
|
firstHop,
|
||||||
|
privateKey: firstKey,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
knownHostFingerprints: hostKeyCache,
|
||||||
|
onHostKeyAccepted: hostKeyPersist,
|
||||||
|
onHostKeyPrompt: hostKeyPrompt,
|
||||||
|
visited: visited,
|
||||||
|
followJumpConfig: false,
|
||||||
|
);
|
||||||
|
createdClients.add(currentClient);
|
||||||
|
|
||||||
|
for (var i = 1; i < hops.length; i++) {
|
||||||
|
final hop = hops[i];
|
||||||
|
final forwarded = await currentClient!.forwardLocal(hop.ip, hop.port);
|
||||||
|
final hopKey = resolveHopPrivateKey(hop);
|
||||||
|
if (hop.keyId != null && hopKey == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(hop.keyId ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
currentClient = await _genClientInternal(
|
||||||
|
hop,
|
||||||
|
privateKey: hopKey,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
knownHostFingerprints: hostKeyCache,
|
||||||
|
onHostKeyAccepted: hostKeyPersist,
|
||||||
|
onHostKeyPrompt: hostKeyPrompt,
|
||||||
|
visited: visited,
|
||||||
|
socketOverride: forwarded,
|
||||||
|
followJumpConfig: false,
|
||||||
|
);
|
||||||
|
createdClients.add(currentClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
final forwardedSocket = await currentClient!.forwardLocal(spi.ip, spi.port);
|
||||||
|
return (forwardedSocket, createdClients);
|
||||||
|
} catch (e) {
|
||||||
|
// Close all created clients on error to avoid leaks
|
||||||
|
for (final client in createdClients) {
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore close errors during cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
// Note: On success, all intermediate clients must remain open
|
||||||
|
// because the returned socket tunnels through them.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct
|
// Direct
|
||||||
try {
|
try {
|
||||||
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
return (await SSHSocket.connect(spi.ip, spi.port, timeout: timeout), <SSHClient>[]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Loggers.app.warning('genClient', e);
|
Loggers.app.warning('genClient', e);
|
||||||
if (spi.alterUrl == null) rethrow;
|
if (spi.alterUrl == null) rethrow;
|
||||||
try {
|
try {
|
||||||
final res = spi.parseAlterUrl();
|
final res = spi.parseAlterUrl();
|
||||||
alterUser = res.$2;
|
alterUser = res.$2;
|
||||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
return (await SSHSocket.connect(res.$1, res.$3, timeout: timeout), <SSHClient>[]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Loggers.app.warning('genClient alterUrl', e);
|
Loggers.app.warning('genClient alterUrl', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -113,32 +311,52 @@ Future<SSHClient> genClient(
|
|||||||
prompt: hostKeyPrompt,
|
prompt: hostKeyPrompt,
|
||||||
);
|
);
|
||||||
|
|
||||||
final keyId = spi.keyId;
|
Future<SSHClient> buildClient(SSHSocket socket) async {
|
||||||
if (keyId == null) {
|
final keyId = spi.keyId;
|
||||||
onStatus?.call(GenSSHClientStatus.pwd);
|
if (keyId == null) {
|
||||||
|
onStatus?.call(GenSSHClientStatus.pwd);
|
||||||
|
return SSHClient(
|
||||||
|
socket,
|
||||||
|
username: alterUser ?? spi.user,
|
||||||
|
onPasswordRequest: () => spi.pwd,
|
||||||
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
|
// printDebug: debugPrint,
|
||||||
|
// printTrace: debugPrint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
privateKey ??= getPrivateKey(keyId);
|
||||||
|
|
||||||
|
onStatus?.call(GenSSHClientStatus.key);
|
||||||
return SSHClient(
|
return SSHClient(
|
||||||
socket,
|
socket,
|
||||||
username: alterUser ?? spi.user,
|
username: spi.user,
|
||||||
onPasswordRequest: () => spi.pwd,
|
// Must use [compute] here, instead of [Computer.shared.start]
|
||||||
|
identities: await compute(loadIndentity, privateKey!),
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
onVerifyHostKey: hostKeyVerifier.call,
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
// printDebug: debugPrint,
|
// printDebug: debugPrint,
|
||||||
// printTrace: debugPrint,
|
// printTrace: debugPrint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
privateKey ??= getPrivateKey(keyId);
|
|
||||||
|
|
||||||
onStatus?.call(GenSSHClientStatus.key);
|
final client = await buildClient(socket);
|
||||||
return SSHClient(
|
|
||||||
socket,
|
// Tie hop clients' lifetime to the final client: close all hop clients
|
||||||
username: spi.user,
|
// when the target client disconnects to avoid leaking SSH connections.
|
||||||
// Must use [compute] here, instead of [Computer.shared.start]
|
if (hopClients.isNotEmpty) {
|
||||||
identities: await compute(loadIndentity, privateKey),
|
client.done.whenComplete(() {
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
for (final hopClient in hopClients) {
|
||||||
onVerifyHostKey: hostKeyVerifier.call,
|
try {
|
||||||
// printDebug: debugPrint,
|
hopClient.close();
|
||||||
// printTrace: debugPrint,
|
} catch (_) {
|
||||||
);
|
// Ignore close errors during cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _HostKeyPersistCallback = void Function(String storageKey, String fingerprintHex);
|
typedef _HostKeyPersistCallback = void Function(String storageKey, String fingerprintHex);
|
||||||
@@ -300,20 +518,53 @@ Future<void> ensureKnownHostKey(
|
|||||||
Duration timeout = const Duration(seconds: 5),
|
Duration timeout = const Duration(seconds: 5),
|
||||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
}) async {
|
}) async {
|
||||||
final cache = _loadKnownHostFingerprints();
|
var cache = _loadKnownHostFingerprints();
|
||||||
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final jumpSpi = spi.jumpId != null ? Stores.server.box.get(spi.jumpId) : null;
|
final hops = resolveMergedJumpChain(spi);
|
||||||
if (jumpSpi != null && !_hasKnownHostFingerprintForSpi(jumpSpi, cache)) {
|
|
||||||
await ensureKnownHostKey(
|
// Check each hop's host key, routing through preceding hops
|
||||||
jumpSpi,
|
for (var i = 0; i < hops.length; i++) {
|
||||||
|
final hop = hops[i];
|
||||||
|
// Preceding hops needed to reach this hop
|
||||||
|
final precedingHops = i > 0 ? hops.sublist(0, i) : null;
|
||||||
|
final precedingKeys = precedingHops?.map((h) =>
|
||||||
|
h.keyId != null ? getPrivateKey(h.keyId!) : null
|
||||||
|
).toList();
|
||||||
|
|
||||||
|
cache = await _ensureKnownHostKeyForSingle(
|
||||||
|
hop,
|
||||||
|
cache: cache,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
onKeyboardInteractive: onKeyboardInteractive,
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
jumpChain: precedingHops,
|
||||||
|
jumpPrivateKeys: precedingKeys,
|
||||||
);
|
);
|
||||||
cache.addAll(_loadKnownHostFingerprints());
|
}
|
||||||
if (_hasKnownHostFingerprintForSpi(spi, cache)) return;
|
|
||||||
|
// Check the target's host key, routing through all hops
|
||||||
|
final allKeys = hops.isNotEmpty
|
||||||
|
? hops.map((h) => h.keyId != null ? getPrivateKey(h.keyId!) : null).toList()
|
||||||
|
: null;
|
||||||
|
await _ensureKnownHostKeyForSingle(
|
||||||
|
spi,
|
||||||
|
cache: cache,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
jumpChain: hops.isNotEmpty ? hops : null,
|
||||||
|
jumpPrivateKeys: allKeys,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> _ensureKnownHostKeyForSingle(
|
||||||
|
Spi spi, {
|
||||||
|
required Map<String, String> cache,
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
|
List<Spi>? jumpChain,
|
||||||
|
List<String?>? jumpPrivateKeys,
|
||||||
|
}) async {
|
||||||
|
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
|
||||||
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
final client = await genClient(
|
final client = await genClient(
|
||||||
@@ -321,6 +572,8 @@ Future<void> ensureKnownHostKey(
|
|||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
onKeyboardInteractive: onKeyboardInteractive,
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
knownHostFingerprints: cache,
|
knownHostFingerprints: cache,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -328,6 +581,9 @@ Future<void> ensureKnownHostKey(
|
|||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.addAll(_loadKnownHostFingerprints());
|
||||||
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasKnownHostFingerprintForSpi(Spi spi, Map<String, String> cache) {
|
bool _hasKnownHostFingerprintForSpi(Spi spi, Map<String, String> cache) {
|
||||||
|
|||||||
@@ -149,10 +149,12 @@ abstract final class SSHConfig {
|
|||||||
|
|
||||||
/// Extract jump host from ProxyJump or ProxyCommand
|
/// Extract jump host from ProxyJump or ProxyCommand
|
||||||
static String? _extractJumpHost(String value) {
|
static String? _extractJumpHost(String value) {
|
||||||
|
if (value.isEmpty) return null;
|
||||||
// For ProxyJump, the format is usually: user@host:port
|
// For ProxyJump, the format is usually: user@host:port
|
||||||
// For ProxyCommand, it's more complex and might need custom parsing
|
// For ProxyCommand, it's more complex and might need custom parsing
|
||||||
if (value.contains('@')) {
|
if (value.contains('@')) {
|
||||||
return value.split(' ').first;
|
final parts = value.split(' ');
|
||||||
|
return parts.isNotEmpty ? parts[0] : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class BackupService {
|
|||||||
await _confirmAndRestore(context, backup);
|
await _confirmAndRestore(context, backup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
// Saved password failed, will prompt for manual input
|
Loggers.app.warning('Failed to restore with saved password, will prompt for manual input', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
lib/data/model/app/menu/platform.dart
Normal file
119
lib/data/model/app/menu/platform.dart
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/data/model/app/tab.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/data/res/url.dart';
|
||||||
|
import 'package:server_box/generated/l10n/l10n.dart';
|
||||||
|
import 'package:server_box/view/page/setting/entry.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
/// macOS Menu Bar
|
||||||
|
class MacOSMenuBarManager {
|
||||||
|
static List<PlatformMenu> buildMenuBar(BuildContext context, Function(int) onTabChanged) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final homeTabs = Stores.setting.homeTabs.fetch();
|
||||||
|
return [
|
||||||
|
PlatformMenu(
|
||||||
|
label: 'Server Box',
|
||||||
|
menus: [
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: libL10n.about,
|
||||||
|
onSelected: () => _showAboutDialog(context),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuSettings,
|
||||||
|
shortcut: const SingleActivator(LogicalKeyboardKey.comma, meta: true),
|
||||||
|
onSelected: () => _openSettings(context),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuQuit,
|
||||||
|
shortcut: const SingleActivator(LogicalKeyboardKey.keyQ, meta: true),
|
||||||
|
onSelected: () => SystemNavigator.pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
PlatformMenu(
|
||||||
|
label: l10n.menuNavigate,
|
||||||
|
menus: _buildNavigateMenuItems(l10n, homeTabs, onTabChanged),
|
||||||
|
),
|
||||||
|
PlatformMenu(
|
||||||
|
label: l10n.menuInfo,
|
||||||
|
menus: [
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuGitHubRepository,
|
||||||
|
onSelected: () => _openURL(Urls.thisRepo),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuWiki,
|
||||||
|
onSelected: () => _openURL(Urls.appWiki),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuHelp,
|
||||||
|
onSelected: () => _openURL(Urls.appHelp),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PlatformMenuItem> _buildNavigateMenuItems(
|
||||||
|
AppLocalizations l10n,
|
||||||
|
List<AppTab> homeTabs,
|
||||||
|
Function(int) onTabChanged,
|
||||||
|
) {
|
||||||
|
final menuItems = <PlatformMenuItem>[];
|
||||||
|
final tabLabels = {
|
||||||
|
AppTab.server: l10n.server,
|
||||||
|
AppTab.ssh: 'SSH',
|
||||||
|
AppTab.file: libL10n.file,
|
||||||
|
AppTab.snippet: l10n.snippet,
|
||||||
|
};
|
||||||
|
for (var i = 0; i < homeTabs.length; i++) {
|
||||||
|
final tab = homeTabs[i];
|
||||||
|
final label = tabLabels[tab];
|
||||||
|
if (label == null) continue;
|
||||||
|
final shortcutKey = _getShortcutKeyForIndex(i);
|
||||||
|
menuItems.add(PlatformMenuItem(
|
||||||
|
label: label,
|
||||||
|
shortcut: shortcutKey != null
|
||||||
|
? SingleActivator(shortcutKey, meta: true)
|
||||||
|
: null,
|
||||||
|
onSelected: () => onTabChanged(i),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return menuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LogicalKeyboardKey? _getShortcutKeyForIndex(int index) {
|
||||||
|
const keys = [
|
||||||
|
LogicalKeyboardKey.digit1,
|
||||||
|
LogicalKeyboardKey.digit2,
|
||||||
|
LogicalKeyboardKey.digit3,
|
||||||
|
LogicalKeyboardKey.digit4,
|
||||||
|
LogicalKeyboardKey.digit5,
|
||||||
|
LogicalKeyboardKey.digit6,
|
||||||
|
LogicalKeyboardKey.digit7,
|
||||||
|
LogicalKeyboardKey.digit8,
|
||||||
|
LogicalKeyboardKey.digit9,
|
||||||
|
];
|
||||||
|
return index < keys.length ? keys[index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _showAboutDialog(BuildContext context) async {
|
||||||
|
const channel = MethodChannel('about');
|
||||||
|
await channel.invokeMethod('showAboutPanel');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _openSettings(BuildContext context) {
|
||||||
|
SettingsPage.route.go(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _openURL(String url) async {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ sealed class ContainerPs {
|
|||||||
|
|
||||||
factory ContainerPs.fromRaw(String s, ContainerType typ) => typ.ps(s);
|
factory ContainerPs.fromRaw(String s, ContainerType typ) => typ.ps(s);
|
||||||
|
|
||||||
void parseStats(String s);
|
void parseStats(String s, [String? version]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PodmanPs implements ContainerPs {
|
final class PodmanPs implements ContainerPs {
|
||||||
@@ -55,7 +55,7 @@ final class PodmanPs implements ContainerPs {
|
|||||||
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
|
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void parseStats(String s) {
|
void parseStats(String s, [String? version]) {
|
||||||
final stats = json.decode(s);
|
final stats = json.decode(s);
|
||||||
final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);
|
final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);
|
||||||
final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);
|
final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);
|
||||||
@@ -63,12 +63,32 @@ final class PodmanPs implements ContainerPs {
|
|||||||
final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;
|
final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;
|
||||||
final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;
|
final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;
|
||||||
mem = '$memUsage / $memLimit';
|
mem = '$memUsage / $memLimit';
|
||||||
final netIn = (stats['NetInput'] as int? ?? 0).bytes2Str;
|
|
||||||
final netOut = (stats['NetOutput'] as int? ?? 0).bytes2Str;
|
int netIn = 0;
|
||||||
net = '↓ $netIn / ↑ $netOut';
|
int netOut = 0;
|
||||||
|
final majorVersion = version?.split('.').firstOrNull;
|
||||||
|
final majorVersionNum = majorVersion != null ? int.tryParse(majorVersion) : null;
|
||||||
|
|
||||||
|
// Podman 4.x and earlier use top-level NetInput/NetOutput fields.
|
||||||
|
// Podman 5.x changed network backend (Netavark) and uses nested
|
||||||
|
// Network.{iface}.RxBytes/TxBytes structure instead.
|
||||||
|
if (majorVersionNum == null || majorVersionNum <= 4) {
|
||||||
|
netIn = stats['NetInput'] as int? ?? 0;
|
||||||
|
netOut = stats['NetOutput'] as int? ?? 0;
|
||||||
|
} else if (majorVersionNum >= 5) {
|
||||||
|
final network = stats['Network'] as Map<String, dynamic>?;
|
||||||
|
if (network != null) {
|
||||||
|
for (final interface in network.values) {
|
||||||
|
netIn += interface['RxBytes'] as int? ?? 0;
|
||||||
|
netOut += interface['TxBytes'] as int? ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
net = '↓ ${netIn.bytes2Str} / ↑ ${netOut.bytes2Str}';
|
||||||
|
|
||||||
final diskIn = (stats['BlockInput'] as int? ?? 0).bytes2Str;
|
final diskIn = (stats['BlockInput'] as int? ?? 0).bytes2Str;
|
||||||
final diskOut = (stats['BlockOutput'] as int? ?? 0).bytes2Str;
|
final diskOut = (stats['BlockOutput'] as int? ?? 0).bytes2Str;
|
||||||
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
|
disk = '${l10n.read} $diskIn / ${l10n.write} $diskOut';
|
||||||
}
|
}
|
||||||
|
|
||||||
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
|
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
|
||||||
@@ -125,12 +145,18 @@ final class DockerPs implements ContainerPs {
|
|||||||
ContainerStatus get status => ContainerStatus.fromDockerState(state);
|
ContainerStatus get status => ContainerStatus.fromDockerState(state);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void parseStats(String s) {
|
void parseStats(String s, [String? version]) {
|
||||||
final stats = json.decode(s);
|
final stats = json.decode(s);
|
||||||
cpu = stats['CPUPerc'];
|
cpu = stats['CPUPerc'];
|
||||||
mem = stats['MemUsage'];
|
mem = stats['MemUsage'];
|
||||||
net = stats['NetIO'];
|
|
||||||
disk = stats['BlockIO'];
|
final netIO = stats['NetIO'] as String? ?? '0B / 0B';
|
||||||
|
final netParts = netIO.split(' / ');
|
||||||
|
net = '↓ ${netParts.firstOrNull ?? '0B'} / ↑ ${netParts.length > 1 ? netParts[1] : '0B'}';
|
||||||
|
|
||||||
|
final blockIO = stats['BlockIO'] as String? ?? '0B / 0B';
|
||||||
|
final blockParts = blockIO.split(' / ');
|
||||||
|
disk = '${l10n.read} ${blockParts.firstOrNull ?? '0B'} / ${l10n.write} ${blockParts.length > 1 ? blockParts[1] : '0B'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CONTAINER ID NAMES IMAGE STATUS
|
/// CONTAINER ID NAMES IMAGE STATUS
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class UpgradePkgInfo {
|
|||||||
|
|
||||||
void _parsePacman(String raw) {
|
void _parsePacman(String raw) {
|
||||||
final parts = raw.split(' ');
|
final parts = raw.split(' ');
|
||||||
|
if (parts.length < 4) throw Exception('Invalid pacman output format');
|
||||||
package = parts[0];
|
package = parts[0];
|
||||||
nowVersion = parts[1];
|
nowVersion = parts[1];
|
||||||
newVersion = parts[3];
|
newVersion = parts[3];
|
||||||
@@ -70,6 +71,7 @@ class UpgradePkgInfo {
|
|||||||
|
|
||||||
void _parseOpkg(String raw) {
|
void _parseOpkg(String raw) {
|
||||||
final parts = raw.split(' - ');
|
final parts = raw.split(' - ');
|
||||||
|
if (parts.length < 3) throw Exception('Invalid opkg output format');
|
||||||
package = parts[0];
|
package = parts[0];
|
||||||
nowVersion = parts[1];
|
nowVersion = parts[1];
|
||||||
newVersion = parts[2];
|
newVersion = parts[2];
|
||||||
@@ -80,6 +82,7 @@ class UpgradePkgInfo {
|
|||||||
void _parseApk(String raw) {
|
void _parseApk(String raw) {
|
||||||
final parts = raw.split(' ');
|
final parts = raw.split(' ');
|
||||||
final len = parts.length;
|
final len = parts.length;
|
||||||
|
if (len < 2) throw Exception('Invalid apk output format');
|
||||||
newVersion = parts[len - 1];
|
newVersion = parts[len - 1];
|
||||||
nowVersion = parts[0];
|
nowVersion = parts[0];
|
||||||
newVersion = newVersion.substring(0, newVersion.length - 1);
|
newVersion = newVersion.substring(0, newVersion.length - 1);
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ class Cpus extends TimeSeq<SingleCpuCore> {
|
|||||||
double usedPercent({int coreIdx = 0}) {
|
double usedPercent({int coreIdx = 0}) {
|
||||||
if (now.length != pre.length) return 0;
|
if (now.length != pre.length) return 0;
|
||||||
if (now.isEmpty) return 0;
|
if (now.isEmpty) return 0;
|
||||||
|
if (coreIdx >= now.length) return 0;
|
||||||
try {
|
try {
|
||||||
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
|
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
|
||||||
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
|
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
|
||||||
|
if (totalDelta == 0) return 0;
|
||||||
final used = idleDelta / totalDelta;
|
final used = idleDelta / totalDelta;
|
||||||
return used.isNaN ? 0 : 100 - used * 100;
|
return used.isNaN ? 0 : 100 - used * 100;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -164,6 +166,7 @@ class SingleCpuCore extends TimeSeqIface<SingleCpuCore> {
|
|||||||
final id = item.split(' ').firstOrNull;
|
final id = item.split(' ').firstOrNull;
|
||||||
if (id == null) continue;
|
if (id == null) continue;
|
||||||
final matches = item.replaceFirst(id, '').trim().split(' ');
|
final matches = item.replaceFirst(id, '').trim().split(' ');
|
||||||
|
if (matches.length < 7) continue;
|
||||||
cpus.add(
|
cpus.add(
|
||||||
SingleCpuCore(
|
SingleCpuCore(
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ class NetSpeed extends TimeSeq<NetSpeedPart> {
|
|||||||
final bytesIn = BigInt.parse(bytes.first);
|
final bytesIn = BigInt.parse(bytes.first);
|
||||||
final bytesOut = BigInt.parse(bytes[8]);
|
final bytesOut = BigInt.parse(bytes[8]);
|
||||||
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to parse net speed data: $item', e, s);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ class Proc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get binary {
|
String get binary {
|
||||||
final parts = command.split(' ');
|
final parts = command.trim().split(' ').where((e) => e.isNotEmpty).toList();
|
||||||
return parts[0];
|
return parts.isNotEmpty ? parts[0] : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
import 'package:server_box/data/model/server/custom.dart';
|
import 'package:server_box/data/model/server/custom.dart';
|
||||||
@@ -35,8 +36,15 @@ abstract class Spi with _$Spi {
|
|||||||
String? alterUrl,
|
String? alterUrl,
|
||||||
@Default(true) bool autoConnect,
|
@Default(true) bool autoConnect,
|
||||||
|
|
||||||
/// [id] of the jump server
|
/// [id] of the jump server (legacy, single hop)
|
||||||
|
///
|
||||||
|
/// Migrated to [jumpChainIds].
|
||||||
String? jumpId,
|
String? jumpId,
|
||||||
|
|
||||||
|
/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
@JsonKey(includeIfNull: false) List<String>? jumpChainIds,
|
||||||
ServerCustom? custom,
|
ServerCustom? custom,
|
||||||
WakeOnLanCfg? wolCfg,
|
WakeOnLanCfg? wolCfg,
|
||||||
|
|
||||||
@@ -79,7 +87,10 @@ extension Spix on Spi {
|
|||||||
String? migrateId() {
|
String? migrateId() {
|
||||||
if (id.isNotEmpty) return null;
|
if (id.isNotEmpty) return null;
|
||||||
ServerStore.instance.delete(oldId);
|
ServerStore.instance.delete(oldId);
|
||||||
final newSpi = copyWith(id: ShortId.generate());
|
final newSpi = copyWith(
|
||||||
|
id: ShortId.generate(),
|
||||||
|
jumpChainIds: jumpChainIds ?? (jumpId == null ? null : [jumpId!]),
|
||||||
|
);
|
||||||
newSpi.save();
|
newSpi.save();
|
||||||
return newSpi.id;
|
return newSpi.id;
|
||||||
}
|
}
|
||||||
@@ -94,7 +105,8 @@ extension Spix on Spi {
|
|||||||
port == other.port &&
|
port == other.port &&
|
||||||
pwd == other.pwd &&
|
pwd == other.pwd &&
|
||||||
keyId == other.keyId &&
|
keyId == other.keyId &&
|
||||||
jumpId == other.jumpId;
|
jumpId == other.jumpId &&
|
||||||
|
listEquals(jumpChainIds, other.jumpChainIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the connection should be re-established.
|
/// Returns true if the connection should be re-established.
|
||||||
@@ -137,7 +149,7 @@ extension Spix on Spi {
|
|||||||
tags: ['tag1', 'tag2'],
|
tags: ['tag1', 'tag2'],
|
||||||
alterUrl: 'user@ip:port',
|
alterUrl: 'user@ip:port',
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
jumpId: 'jump_server_id',
|
jumpChainIds: ['jump_server_id'],
|
||||||
custom: ServerCustom(
|
custom: ServerCustom(
|
||||||
pveAddr: 'http://localhost:8006',
|
pveAddr: 'http://localhost:8006',
|
||||||
pveIgnoreCert: false,
|
pveIgnoreCert: false,
|
||||||
|
|||||||
@@ -16,8 +16,13 @@ T _$identity<T>(T value) => value;
|
|||||||
mixin _$Spi {
|
mixin _$Spi {
|
||||||
|
|
||||||
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
|
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
|
||||||
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
|
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server (legacy, single hop)
|
||||||
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
///
|
||||||
|
/// Migrated to [jumpChainIds].
|
||||||
|
String? get jumpId;/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
@JsonKey(includeIfNull: false) List<String>? get jumpChainIds; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
||||||
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||||
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
|
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
|
||||||
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
|
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
|
||||||
@@ -33,12 +38,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other.jumpChainIds, jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -49,7 +54,7 @@ abstract mixin class $SpiCopyWith<$Res> {
|
|||||||
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +71,7 @@ class _$SpiCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -78,7 +83,8 @@ as String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to
|
|||||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
as String?,jumpChainIds: freezed == jumpChainIds ? _self.jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||||
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
|
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -169,10 +175,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi() when $default != null:
|
case _Spi() when $default != null:
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -190,10 +196,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi():
|
case _Spi():
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -210,10 +216,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi() when $default != null:
|
case _Spi() when $default != null:
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -225,7 +231,7 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
|
|
||||||
@JsonSerializable(includeIfNull: false)
|
@JsonSerializable(includeIfNull: false)
|
||||||
class _Spi extends Spi {
|
class _Spi extends Spi {
|
||||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, @JsonKey(includeIfNull: false) final List<String>? jumpChainIds, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_jumpChainIds = jumpChainIds,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
||||||
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||||
|
|
||||||
@override final String name;
|
@override final String name;
|
||||||
@@ -246,8 +252,25 @@ class _Spi extends Spi {
|
|||||||
|
|
||||||
@override final String? alterUrl;
|
@override final String? alterUrl;
|
||||||
@override@JsonKey() final bool autoConnect;
|
@override@JsonKey() final bool autoConnect;
|
||||||
/// [id] of the jump server
|
/// [id] of the jump server (legacy, single hop)
|
||||||
|
///
|
||||||
|
/// Migrated to [jumpChainIds].
|
||||||
@override final String? jumpId;
|
@override final String? jumpId;
|
||||||
|
/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
final List<String>? _jumpChainIds;
|
||||||
|
/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
@override@JsonKey(includeIfNull: false) List<String>? get jumpChainIds {
|
||||||
|
final value = _jumpChainIds;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_jumpChainIds is EqualUnmodifiableListView) return _jumpChainIds;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
@override final ServerCustom? custom;
|
@override final ServerCustom? custom;
|
||||||
@override final WakeOnLanCfg? wolCfg;
|
@override final WakeOnLanCfg? wolCfg;
|
||||||
/// It only applies to SSH terminal.
|
/// It only applies to SSH terminal.
|
||||||
@@ -289,12 +312,12 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other._jumpChainIds, _jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(_jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -305,7 +328,7 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
|
|||||||
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -322,7 +345,7 @@ class __$SpiCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||||
return _then(_Spi(
|
return _then(_Spi(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -334,7 +357,8 @@ as String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_t
|
|||||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
as String?,jumpChainIds: freezed == jumpChainIds ? _self._jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||||
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
|
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
|
|||||||
alterUrl: json['alterUrl'] as String?,
|
alterUrl: json['alterUrl'] as String?,
|
||||||
autoConnect: json['autoConnect'] as bool? ?? true,
|
autoConnect: json['autoConnect'] as bool? ?? true,
|
||||||
jumpId: json['jumpId'] as String?,
|
jumpId: json['jumpId'] as String?,
|
||||||
|
jumpChainIds: (json['jumpChainIds'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
custom: json['custom'] == null
|
custom: json['custom'] == null
|
||||||
? null
|
? null
|
||||||
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
|
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
|
||||||
@@ -47,6 +50,7 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
|||||||
'alterUrl': ?instance.alterUrl,
|
'alterUrl': ?instance.alterUrl,
|
||||||
'autoConnect': instance.autoConnect,
|
'autoConnect': instance.autoConnect,
|
||||||
'jumpId': ?instance.jumpId,
|
'jumpId': ?instance.jumpId,
|
||||||
|
'jumpChainIds': ?instance.jumpChainIds,
|
||||||
'custom': ?instance.custom,
|
'custom': ?instance.custom,
|
||||||
'wolCfg': ?instance.wolCfg,
|
'wolCfg': ?instance.wolCfg,
|
||||||
'envs': ?instance.envs,
|
'envs': ?instance.envs,
|
||||||
|
|||||||
@@ -679,7 +679,7 @@ void _parseWindowsTemperatures(Temperatures temps, String raw) {
|
|||||||
if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
|
if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
|
||||||
temps.parse(typeLines.join('\n'), valueLines.join('\n'));
|
temps.parse(typeLines.join('\n'), valueLines.join('\n'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
// If JSON parsing fails, ignore temperature data
|
Loggers.app.warning('Failed to parse Windows temperature data', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ class SftpReq {
|
|||||||
final String localPath;
|
final String localPath;
|
||||||
final SftpReqType type;
|
final SftpReqType type;
|
||||||
String? privateKey;
|
String? privateKey;
|
||||||
Spi? jumpSpi;
|
List<Spi>? jumpChain;
|
||||||
String? jumpPrivateKey;
|
List<String?>? jumpPrivateKeys;
|
||||||
Map<String, String>? knownHostFingerprints;
|
Map<String, String>? knownHostFingerprints;
|
||||||
|
|
||||||
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
||||||
@@ -15,13 +15,22 @@ class SftpReq {
|
|||||||
if (keyId != null) {
|
if (keyId != null) {
|
||||||
privateKey = getPrivateKey(keyId);
|
privateKey = getPrivateKey(keyId);
|
||||||
}
|
}
|
||||||
if (spi.jumpId != null) {
|
if (spi.jumpChainIds != null || spi.jumpId != null) {
|
||||||
jumpSpi = Stores.server.box.get(spi.jumpId);
|
// Use resolveMergedJumpChain to recursively expand nested hop chains
|
||||||
jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;
|
final chain = resolveMergedJumpChain(spi);
|
||||||
|
final keys = <String?>[];
|
||||||
|
for (final hop in chain) {
|
||||||
|
keys.add(hop.keyId != null ? getPrivateKey(hop.keyId!) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always set when a jump is configured so the isolate won't fallback to Stores.
|
||||||
|
jumpChain = chain;
|
||||||
|
jumpPrivateKeys = keys;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
knownHostFingerprints = Map<String, String>.from(Stores.setting.sshKnownHostFingerprints.get());
|
knownHostFingerprints = Map<String, String>.from(Stores.setting.sshKnownHostFingerprints.get());
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to load SSH known host fingerprints', e, s);
|
||||||
knownHostFingerprints = null;
|
knownHostFingerprints = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +45,7 @@ class SftpReqStatus {
|
|||||||
late SftpWorker worker;
|
late SftpWorker worker;
|
||||||
final Completer? completer;
|
final Completer? completer;
|
||||||
|
|
||||||
String get fileName => req.localPath.split('/').last;
|
String get fileName => req.localPath.split(Pfs.seperator).last;
|
||||||
|
|
||||||
// status of the download
|
// status of the download
|
||||||
double? progress;
|
double? progress;
|
||||||
@@ -89,4 +98,4 @@ class SftpReqStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SftpWorkerStatus { preparing, sshConnectted, loading, finished }
|
enum SftpWorkerStatus { preparing, sshConnected, loading, finished }
|
||||||
|
|||||||
@@ -63,14 +63,14 @@ Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sen
|
|||||||
final client = await genClient(
|
final client = await genClient(
|
||||||
req.spi,
|
req.spi,
|
||||||
privateKey: req.privateKey,
|
privateKey: req.privateKey,
|
||||||
jumpSpi: req.jumpSpi,
|
jumpChain: req.jumpChain,
|
||||||
jumpPrivateKey: req.jumpPrivateKey,
|
jumpPrivateKeys: req.jumpPrivateKeys,
|
||||||
knownHostFingerprints: req.knownHostFingerprints,
|
knownHostFingerprints: req.knownHostFingerprints,
|
||||||
);
|
);
|
||||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
mainSendPort.send(SftpWorkerStatus.sshConnected);
|
||||||
|
|
||||||
/// Create the directory if not exists
|
/// Create the directory if not exists
|
||||||
final dirPath = req.localPath.substring(0, req.localPath.lastIndexOf('/'));
|
final dirPath = req.localPath.substring(0, req.localPath.lastIndexOf(Pfs.seperator));
|
||||||
await Directory(dirPath).create(recursive: true);
|
await Directory(dirPath).create(recursive: true);
|
||||||
|
|
||||||
/// Use [FileMode.write] to overwrite the file
|
/// Use [FileMode.write] to overwrite the file
|
||||||
@@ -120,11 +120,11 @@ Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendE
|
|||||||
final client = await genClient(
|
final client = await genClient(
|
||||||
req.spi,
|
req.spi,
|
||||||
privateKey: req.privateKey,
|
privateKey: req.privateKey,
|
||||||
jumpSpi: req.jumpSpi,
|
jumpChain: req.jumpChain,
|
||||||
jumpPrivateKey: req.jumpPrivateKey,
|
jumpPrivateKeys: req.jumpPrivateKeys,
|
||||||
knownHostFingerprints: req.knownHostFingerprints,
|
knownHostFingerprints: req.knownHostFingerprints,
|
||||||
);
|
);
|
||||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
mainSendPort.send(SftpWorkerStatus.sshConnected);
|
||||||
|
|
||||||
final local = File(req.localPath);
|
final local = File(req.localPath);
|
||||||
if (!await local.exists()) {
|
if (!await local.exists()) {
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class ContainerNotifier extends _$ContainerNotifier {
|
|||||||
(element) => element.contains(id.substring(0, 5)),
|
(element) => element.contains(id.substring(0, 5)),
|
||||||
);
|
);
|
||||||
if (statsLine == null) continue;
|
if (statsLine == null) continue;
|
||||||
item.parseStats(statsLine);
|
item.parseStats(statsLine, state.version);
|
||||||
}
|
}
|
||||||
} catch (e, trace) {
|
} catch (e, trace) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -280,7 +280,7 @@ enum ContainerCmdType {
|
|||||||
return switch (this) {
|
return switch (this) {
|
||||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||||
ContainerCmdType.ps => switch (type) {
|
ContainerCmdType.ps => switch (type) {
|
||||||
/// TODO: Rollback to json format when permformance recovers.
|
/// TODO: Rollback to json format when performance recovers.
|
||||||
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
||||||
ContainerType.docker =>
|
ContainerType.docker =>
|
||||||
'$prefix ps -a --format "table {{printf \\"'
|
'$prefix ps -a --format "table {{printf \\"'
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class ContainerNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
|
String _$containerNotifierHash() => r'85457ec75264199c284572ee45beeaccba2044a1';
|
||||||
|
|
||||||
final class ContainerNotifierFamily extends $Family
|
final class ContainerNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:dartssh2/dartssh2.dart';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
@@ -108,7 +107,7 @@ class PveNotifier extends _$PveNotifier {
|
|||||||
final newUrl = Uri.parse(
|
final newUrl = Uri.parse(
|
||||||
addr,
|
addr,
|
||||||
).replace(host: 'localhost', port: _localPort).toString();
|
).replace(host: 'localhost', port: _localPort).toString();
|
||||||
debugPrint('Forwarding $newUrl to $addr');
|
dprint('Forwarding $newUrl to $addr');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,11 +234,15 @@ class PveNotifier extends _$PveNotifier {
|
|||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
try {
|
try {
|
||||||
await _serverSocket.close();
|
await _serverSocket.close();
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to close server socket', e, s);
|
||||||
|
}
|
||||||
for (final forward in _forwards) {
|
for (final forward in _forwards) {
|
||||||
try {
|
try {
|
||||||
forward.close();
|
forward.close();
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to close forward', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class PveNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$pveNotifierHash() => r'ba5f2d6cb47c33735f7cc09b771b4a86501b86c6';
|
String _$pveNotifierHash() => r'1e71faadee074b9c07bee731ef4ae6505e791967';
|
||||||
|
|
||||||
final class PveNotifierFamily extends $Family
|
final class PveNotifierFamily extends $Family
|
||||||
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
|
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
|
||||||
|
|||||||
@@ -103,37 +103,44 @@ class ServersNotifier extends _$ServersNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait(
|
final serversToRefresh = <MapEntry<String, Spi>>[];
|
||||||
state.servers.entries.map((entry) async {
|
final idsToResetLimiter = <String>[];
|
||||||
final serverId = entry.key;
|
|
||||||
final spi = entry.value;
|
|
||||||
|
|
||||||
if (onlyFailed) {
|
for (final entry in state.servers.entries) {
|
||||||
final serverState = ref.read(serverProvider(serverId));
|
final serverId = entry.key;
|
||||||
if (serverState.conn != ServerConn.failed) return;
|
final spi = entry.value;
|
||||||
TryLimiter.reset(serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.manualDisconnectedIds.contains(serverId)) return;
|
if (state.manualDisconnectedIds.contains(serverId)) continue;
|
||||||
|
|
||||||
final serverState = ref.read(serverProvider(serverId));
|
final serverState = ref.read(serverProvider(serverId));
|
||||||
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final serverNotifier = ref.read(serverProvider(serverId).notifier);
|
if (onlyFailed) {
|
||||||
await serverNotifier.refresh();
|
if (serverState.conn != ServerConn.failed) continue;
|
||||||
}),
|
idsToResetLimiter.add(serverId);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) continue;
|
||||||
|
|
||||||
|
serversToRefresh.add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final id in idsToResetLimiter) {
|
||||||
|
TryLimiter.reset(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in serversToRefresh) {
|
||||||
|
final serverNotifier = ref.read(serverProvider(entry.key).notifier);
|
||||||
|
serverNotifier.refresh().ignore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startAutoRefresh() async {
|
Future<void> startAutoRefresh() async {
|
||||||
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
if (duration == 0) return;
|
if (duration == 0) return;
|
||||||
if (duration < 0 || duration > 10 || duration == 1) {
|
if (duration <= 1 || duration > 10) {
|
||||||
duration = 3;
|
|
||||||
Loggers.app.warning('Invalid duration: $duration, use default 3');
|
Loggers.app.warning('Invalid duration: $duration, use default 3');
|
||||||
|
duration = 3;
|
||||||
}
|
}
|
||||||
final timer = Timer.periodic(Duration(seconds: duration), (_) async {
|
final timer = Timer.periodic(Duration(seconds: duration), (_) async {
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -145,8 +152,8 @@ class ServersNotifier extends _$ServersNotifier {
|
|||||||
final timer = state.autoRefreshTimer;
|
final timer = state.autoRefreshTimer;
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
state = state.copyWith(autoRefreshTimer: null);
|
|
||||||
}
|
}
|
||||||
|
state = state.copyWith(autoRefreshTimer: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isAutoRefreshOn => state.autoRefreshTimer != null;
|
bool get isAutoRefreshOn => state.autoRefreshTimer != null;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class ServersNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$serversNotifierHash() => r'3292bdce7d602ff64687b05ff81d120e71761ec2';
|
String _$serversNotifierHash() => r'277d1b219235f14bcc1b82a1e16260c2f28decdb';
|
||||||
|
|
||||||
abstract class _$ServersNotifier extends $Notifier<ServersState> {
|
abstract class _$ServersNotifier extends $Notifier<ServersState> {
|
||||||
ServersState build();
|
ServersState build();
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ abstract class ServerState with _$ServerState {
|
|||||||
required ServerStatus status,
|
required ServerStatus status,
|
||||||
@Default(ServerConn.disconnected) ServerConn conn,
|
@Default(ServerConn.disconnected) ServerConn conn,
|
||||||
SSHClient? client,
|
SSHClient? client,
|
||||||
Future<void>? updateFuture,
|
|
||||||
}) = _ServerState;
|
}) = _ServerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,19 +80,16 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refresh server status
|
// Refresh server status
|
||||||
|
bool _isRefreshing = false;
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
if (state.updateFuture != null) {
|
if (_isRefreshing) return;
|
||||||
await state.updateFuture;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final updateFuture = _updateServer();
|
|
||||||
state = state.copyWith(updateFuture: updateFuture);
|
|
||||||
|
|
||||||
|
_isRefreshing = true;
|
||||||
try {
|
try {
|
||||||
await updateFuture;
|
await _updateServer();
|
||||||
} finally {
|
} finally {
|
||||||
state = state.copyWith(updateFuture: null);
|
_isRefreshing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +135,7 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
|
|
||||||
final time2 = DateTime.now();
|
final time2 = DateTime.now();
|
||||||
final spentTime = time2.difference(time1).inMilliseconds;
|
final spentTime = time2.difference(time1).inMilliseconds;
|
||||||
if (spi.jumpId == null) {
|
if ((spi.jumpChainIds?.isNotEmpty != true) && spi.jumpId == null) {
|
||||||
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
||||||
} else {
|
} else {
|
||||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
||||||
@@ -292,7 +288,7 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final statusCmd = ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir);
|
final statusCmd = ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir);
|
||||||
Loggers.app.info('Running status command for ${spi.name} (${state.status.system.name}): $statusCmd');
|
// Loggers.app.info('Running status command for ${spi.name} (${state.status.system.name}): $statusCmd');
|
||||||
final execResult = await state.client?.run(statusCmd);
|
final execResult = await state.client?.run(statusCmd);
|
||||||
if (execResult != null) {
|
if (execResult != null) {
|
||||||
raw = SSHDecoder.decode(
|
raw = SSHDecoder.decode(
|
||||||
@@ -300,7 +296,7 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
isWindows: state.status.system == SystemType.windows,
|
isWindows: state.status.system == SystemType.windows,
|
||||||
context: 'GetStatus<${spi.name}>',
|
context: 'GetStatus<${spi.name}>',
|
||||||
);
|
);
|
||||||
Loggers.app.info('Status response length for ${spi.name}: ${raw.length} bytes');
|
// Loggers.app.info('Status response length for ${spi.name}: ${raw.length} bytes');
|
||||||
} else {
|
} else {
|
||||||
raw = '';
|
raw = '';
|
||||||
Loggers.app.warning('No status result from ${spi.name}');
|
Loggers.app.warning('No status result from ${spi.name}');
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$ServerState {
|
mixin _$ServerState {
|
||||||
|
|
||||||
Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client; Future<void>? get updateFuture;
|
Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client;
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -25,16 +25,16 @@ $ServerStateCopyWith<ServerState> get copyWith => _$ServerStateCopyWithImpl<Serv
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
|
int get hashCode => Object.hash(runtimeType,spi,status,conn,client);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
|
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ abstract mixin class $ServerStateCopyWith<$Res> {
|
|||||||
factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;
|
factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
|
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -62,14 +62,13 @@ class _$ServerStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
||||||
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||||
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
|
as SSHClient?,
|
||||||
as Future<void>?,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
@@ -163,10 +162,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _ServerState() when $default != null:
|
case _ServerState() when $default != null:
|
||||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -184,10 +183,10 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _ServerState():
|
case _ServerState():
|
||||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -204,10 +203,10 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _ServerState() when $default != null:
|
case _ServerState() when $default != null:
|
||||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -219,14 +218,13 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
|
|||||||
|
|
||||||
|
|
||||||
class _ServerState implements ServerState {
|
class _ServerState implements ServerState {
|
||||||
const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client, this.updateFuture});
|
const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client});
|
||||||
|
|
||||||
|
|
||||||
@override final Spi spi;
|
@override final Spi spi;
|
||||||
@override final ServerStatus status;
|
@override final ServerStatus status;
|
||||||
@override@JsonKey() final ServerConn conn;
|
@override@JsonKey() final ServerConn conn;
|
||||||
@override final SSHClient? client;
|
@override final SSHClient? client;
|
||||||
@override final Future<void>? updateFuture;
|
|
||||||
|
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -238,16 +236,16 @@ _$ServerStateCopyWith<_ServerState> get copyWith => __$ServerStateCopyWithImpl<_
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
|
int get hashCode => Object.hash(runtimeType,spi,status,conn,client);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
|
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -258,7 +256,7 @@ abstract mixin class _$ServerStateCopyWith<$Res> implements $ServerStateCopyWith
|
|||||||
factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;
|
factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
|
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -275,14 +273,13 @@ class __$ServerStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {
|
||||||
return _then(_ServerState(
|
return _then(_ServerState(
|
||||||
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
||||||
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||||
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
|
as SSHClient?,
|
||||||
as Future<void>?,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class ServerNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$serverNotifierHash() => r'185c6b4546c3bc526f5b2ca79d16aed665818863';
|
String _$serverNotifierHash() => r'52e806bcc32a7818d1ec2b07a3c683b06885c9f8';
|
||||||
|
|
||||||
final class ServerNotifierFamily extends $Family
|
final class ServerNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
|
|
||||||
abstract class BuildData {
|
abstract class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 1276;
|
static const int build = 1291;
|
||||||
static const int script = 70;
|
static const int script = 70;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ abstract final class GithubIds {
|
|||||||
'MasedMSD',
|
'MasedMSD',
|
||||||
'GitGitro',
|
'GitGitro',
|
||||||
'Shin-suechtig',
|
'Shin-suechtig',
|
||||||
|
'GT-610'
|
||||||
};
|
};
|
||||||
|
|
||||||
static const participants = <GhId>{
|
static const participants = <GhId>{
|
||||||
|
|||||||
@@ -89,15 +89,12 @@ class ServerStore extends HiveStore {
|
|||||||
// Replace ids in jump server settings.
|
// Replace ids in jump server settings.
|
||||||
final spi = get<Spi>(newId);
|
final spi = get<Spi>(newId);
|
||||||
if (spi != null) {
|
if (spi != null) {
|
||||||
final jumpId = spi.jumpId; // This could be an oldId.
|
final jumpChainIds = spi.jumpChainIds ?? (spi.jumpId == null ? null : [spi.jumpId!]);
|
||||||
// Check if this jumpId corresponds to a server that was also migrated.
|
if (jumpChainIds == null || jumpChainIds.isEmpty) continue;
|
||||||
if (jumpId != null && idMap.containsKey(jumpId)) {
|
|
||||||
final newJumpId = idMap[jumpId];
|
final newChain = jumpChainIds.map((e) => idMap[e] ?? e).toList();
|
||||||
if (spi.jumpId != newJumpId) {
|
final newSpi = spi.copyWith(jumpId: null, jumpChainIds: newChain);
|
||||||
final newSpi = spi.copyWith(jumpId: newJumpId);
|
update(spi, newSpi);
|
||||||
update(spi, newSpi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace ids in [Snippet]
|
// Replace ids in [Snippet]
|
||||||
|
|||||||
@@ -1885,6 +1885,54 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
||||||
String get writeScriptTip;
|
String get writeScriptTip;
|
||||||
|
|
||||||
|
/// No description provided for @menuSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Setting'**
|
||||||
|
String get menuSettings;
|
||||||
|
|
||||||
|
/// No description provided for @menuQuit.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Quit'**
|
||||||
|
String get menuQuit;
|
||||||
|
|
||||||
|
/// No description provided for @menuNavigate.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Navigate'**
|
||||||
|
String get menuNavigate;
|
||||||
|
|
||||||
|
/// No description provided for @menuInfo.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Info'**
|
||||||
|
String get menuInfo;
|
||||||
|
|
||||||
|
/// No description provided for @menuGitHubRepository.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'GitHub Repository'**
|
||||||
|
String get menuGitHubRepository;
|
||||||
|
|
||||||
|
/// No description provided for @menuWiki.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Wiki'**
|
||||||
|
String get menuWiki;
|
||||||
|
|
||||||
|
/// No description provided for @menuHelp.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Help'**
|
||||||
|
String get menuHelp;
|
||||||
|
|
||||||
|
/// No description provided for @logs.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Logs'**
|
||||||
|
String get logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -1007,4 +1007,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Protokolle';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -998,4 +998,28 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Logs';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1009,4 +1009,28 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Registros';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1012,4 +1012,28 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
|
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Journaux';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -998,4 +998,28 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Log';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -967,5 +967,29 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
'サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'ログ';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1005,4 +1005,28 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Logboeken';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1000,4 +1000,28 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Logs';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1004,4 +1004,28 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Журналы';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -999,4 +999,28 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Günlükler';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1004,4 +1004,28 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Журнали';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -953,6 +953,30 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => '设置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => '退出';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => '导航';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => '信息';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub 仓库';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => '帮助';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => '日志';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||||
@@ -1904,4 +1928,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => '日誌';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
alterUrl: fields[7] as String?,
|
alterUrl: fields[7] as String?,
|
||||||
autoConnect: fields[8] == null ? true : fields[8] as bool,
|
autoConnect: fields[8] == null ? true : fields[8] as bool,
|
||||||
jumpId: fields[9] as String?,
|
jumpId: fields[9] as String?,
|
||||||
|
jumpChainIds: (fields[16] as List?)?.cast<String>(),
|
||||||
custom: fields[10] as ServerCustom?,
|
custom: fields[10] as ServerCustom?,
|
||||||
wolCfg: fields[11] as WakeOnLanCfg?,
|
wolCfg: fields[11] as WakeOnLanCfg?,
|
||||||
envs: (fields[12] as Map?)?.cast<String, String>(),
|
envs: (fields[12] as Map?)?.cast<String, String>(),
|
||||||
@@ -119,7 +120,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, Spi obj) {
|
void write(BinaryWriter writer, Spi obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(16)
|
..writeByte(17)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.name)
|
..write(obj.name)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -151,7 +152,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
..writeByte(14)
|
..writeByte(14)
|
||||||
..write(obj.customSystemType)
|
..write(obj.customSystemType)
|
||||||
..writeByte(15)
|
..writeByte(15)
|
||||||
..write(obj.disabledCmdTypes);
|
..write(obj.disabledCmdTypes)
|
||||||
|
..writeByte(16)
|
||||||
|
..write(obj.jumpChainIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ types:
|
|||||||
index: 4
|
index: 4
|
||||||
Spi:
|
Spi:
|
||||||
typeId: 3
|
typeId: 3
|
||||||
nextIndex: 16
|
nextIndex: 17
|
||||||
fields:
|
fields:
|
||||||
name:
|
name:
|
||||||
index: 0
|
index: 0
|
||||||
@@ -61,6 +61,8 @@ types:
|
|||||||
index: 14
|
index: 14
|
||||||
disabledCmdTypes:
|
disabledCmdTypes:
|
||||||
index: 15
|
index: 15
|
||||||
|
jumpChainIds:
|
||||||
|
index: 16
|
||||||
VirtKey:
|
VirtKey:
|
||||||
typeId: 4
|
typeId: 4
|
||||||
nextIndex: 45
|
nextIndex: 45
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
||||||
"write": "Schreiben",
|
"write": "Schreiben",
|
||||||
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
||||||
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen."
|
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.",
|
||||||
|
"logs": "Protokolle"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,5 +296,13 @@
|
|||||||
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
||||||
"write": "Write",
|
"write": "Write",
|
||||||
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
||||||
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content."
|
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.",
|
||||||
|
"menuSettings": "Setting",
|
||||||
|
"menuQuit": "Quit",
|
||||||
|
"menuNavigate": "Navigate",
|
||||||
|
"menuInfo": "Info",
|
||||||
|
"menuGitHubRepository": "GitHub Repository",
|
||||||
|
"menuWiki": "Wiki",
|
||||||
|
"menuHelp": "Help",
|
||||||
|
"logs": "Logs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
||||||
"write": "Escribir",
|
"write": "Escribir",
|
||||||
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
||||||
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script."
|
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.",
|
||||||
|
"logs": "Registros"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
|
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
|
||||||
"write": "Écrire",
|
"write": "Écrire",
|
||||||
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
|
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
|
||||||
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script."
|
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.",
|
||||||
|
"logs": "Journaux"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
||||||
"write": "Tulis",
|
"write": "Tulis",
|
||||||
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
||||||
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut."
|
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.",
|
||||||
|
"logs": "Log"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
||||||
"write": "書き込み",
|
"write": "書き込み",
|
||||||
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
||||||
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。"
|
"writeScriptTip": "サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。",
|
||||||
|
"logs": "ログ"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
||||||
"write": "Schrijven",
|
"write": "Schrijven",
|
||||||
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
||||||
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren."
|
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.",
|
||||||
|
"logs": "Logboeken"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
||||||
"write": "Escrita",
|
"write": "Escrita",
|
||||||
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
||||||
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script."
|
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.",
|
||||||
|
"logs": "Logs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
||||||
"write": "Запись",
|
"write": "Запись",
|
||||||
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
||||||
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
|
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.",
|
||||||
|
"logs": "Журналы"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
||||||
"write": "Yaz",
|
"write": "Yaz",
|
||||||
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
||||||
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz."
|
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.",
|
||||||
|
"logs": "Günlükler"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
||||||
"write": "Записати",
|
"write": "Записати",
|
||||||
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
||||||
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта."
|
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.",
|
||||||
|
"logs": "Журнали"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,13 @@
|
|||||||
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
||||||
"write": "写",
|
"write": "写",
|
||||||
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
||||||
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。",
|
||||||
|
"menuSettings": "设置",
|
||||||
|
"menuQuit": "退出",
|
||||||
|
"menuNavigate": "导航",
|
||||||
|
"menuInfo": "信息",
|
||||||
|
"menuGitHubRepository": "GitHub 仓库",
|
||||||
|
"menuWiki": "Wiki",
|
||||||
|
"menuHelp": "帮助",
|
||||||
|
"logs": "日志"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@
|
|||||||
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
||||||
"write": "寫入",
|
"write": "寫入",
|
||||||
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
||||||
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。",
|
||||||
|
"logs": "日誌"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ void _setupDebug() {
|
|||||||
Logger.root.level = Level.ALL;
|
Logger.root.level = Level.ALL;
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
DebugProvider.addLog(record);
|
DebugProvider.addLog(record);
|
||||||
lprint(record);
|
|
||||||
if (record.error != null) print(record.error);
|
if (record.error != null) print(record.error);
|
||||||
if (record.stackTrace != null) print(record.stackTrace);
|
if (record.stackTrace != null) print(record.stackTrace);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ extension on _ContainerPageState {
|
|||||||
'${switch (_containerState.type) {
|
'${switch (_containerState.type) {
|
||||||
ContainerType.podman => 'podman',
|
ContainerType.podman => 'podman',
|
||||||
ContainerType.docker => 'docker',
|
ContainerType.docker => 'docker',
|
||||||
}} exec -it ${dItem.id} sh',
|
}} exec -it ${dItem.id} sh -c "command -v bash && exec bash || command -v ash && exec ash || exec sh"',
|
||||||
);
|
);
|
||||||
SSHPage.route.go(context, args);
|
SSHPage.route.go(context, args);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/foundation.dart' show kReleaseMode;
|
import 'package:flutter/foundation.dart' show kReleaseMode;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -5,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:server_box/core/chan.dart';
|
import 'package:server_box/core/chan.dart';
|
||||||
import 'package:server_box/core/sync.dart';
|
import 'package:server_box/core/sync.dart';
|
||||||
|
import 'package:server_box/data/model/app/menu/platform.dart';
|
||||||
import 'package:server_box/data/model/app/tab.dart';
|
import 'package:server_box/data/model/app/tab.dart';
|
||||||
import 'package:server_box/data/provider/server/all.dart';
|
import 'package:server_box/data/provider/server/all.dart';
|
||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
@@ -134,7 +137,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
super.build(context);
|
super.build(context);
|
||||||
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||||
|
|
||||||
return Scaffold(
|
final Widget mainContent = Scaffold(
|
||||||
appBar: _AppBar(MediaQuery.paddingOf(context).top),
|
appBar: _AppBar(MediaQuery.paddingOf(context).top),
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -157,6 +160,16 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
),
|
),
|
||||||
bottomNavigationBar: isMobile ? _buildBottomBar() : null,
|
bottomNavigationBar: isMobile ? _buildBottomBar() : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
return PlatformMenuBar(
|
||||||
|
menus: MacOSMenuBarManager.buildMenuBar(context, (int index) {
|
||||||
|
_onDestinationSelected(index);
|
||||||
|
}),
|
||||||
|
child: mainContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return mainContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomBar() {
|
Widget _buildBottomBar() {
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
final _netSortType = ValueNotifier(_NetSortType.device);
|
final _netSortType = ValueNotifier(_NetSortType.device);
|
||||||
late final _collapse = _settings.collapseUIDefault.fetch();
|
late final _collapse = _settings.collapseUIDefault.fetch();
|
||||||
late final _textFactor = TextScaler.linear(_settings.textFactor.fetch());
|
late final _textFactor = TextScaler.linear(_settings.textFactor.fetch());
|
||||||
|
late final _cpuViewAsProgress = _settings.cpuViewAsProgress.fetch();
|
||||||
|
late final _moveServerFuncs = _settings.moveServerFuncs.fetch();
|
||||||
|
late final _displayCpuIndex = _settings.displayCpuIndex.fetch();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -97,7 +100,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMainPage(ServerState si) {
|
Widget _buildMainPage(ServerState si) {
|
||||||
final buildFuncs = !Stores.setting.moveServerFuncs.fetch();
|
final buildFuncs = !_moveServerFuncs;
|
||||||
final logo = _buildLogo(si);
|
final logo = _buildLogo(si);
|
||||||
final children = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];
|
final children = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];
|
||||||
for (final card in _cardsOrder) {
|
for (final card in _cardsOrder) {
|
||||||
@@ -197,7 +200,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Widget> children = Stores.setting.cpuViewAsProgress.fetch()
|
final List<Widget> children = _cpuViewAsProgress
|
||||||
? _buildCPUProgress(ss.cpu)
|
? _buildCPUProgress(ss.cpu)
|
||||||
: [_buildCPUChart(ss)];
|
: [_buildCPUChart(ss)];
|
||||||
|
|
||||||
@@ -258,7 +261,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
const kRowThreshold = 4;
|
const kRowThreshold = 4;
|
||||||
const kCoresCountThreshold = kMaxColumn * kRowThreshold;
|
const kCoresCountThreshold = kMaxColumn * kRowThreshold;
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
final displayCpuIndexSetting = Stores.setting.displayCpuIndex.fetch();
|
final displayCpuIndexSetting = _displayCpuIndex;
|
||||||
|
|
||||||
if (cs.coresCount > kCoresCountThreshold) {
|
if (cs.coresCount > kCoresCountThreshold) {
|
||||||
final numCoresToDisplay = cs.coresCount - 1;
|
final numCoresToDisplay = cs.coresCount - 1;
|
||||||
|
|||||||
@@ -222,6 +222,30 @@ extension _Actions on _ServerEditPageState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final oldSpi = this.spi;
|
||||||
|
if (oldSpi != null) {
|
||||||
|
final originalJumpChain = oldSpi.jumpChainIds ?? (oldSpi.jumpId == null ? const <String>[] : [oldSpi.jumpId!]);
|
||||||
|
final currentJumpChain = _jumpChain.value;
|
||||||
|
|
||||||
|
final jumpChainChanged = () {
|
||||||
|
if (originalJumpChain.isEmpty && currentJumpChain.isEmpty) return false;
|
||||||
|
if (originalJumpChain.length != currentJumpChain.length) return true;
|
||||||
|
for (var i = 0; i < originalJumpChain.length; i++) {
|
||||||
|
if (originalJumpChain[i] != currentJumpChain[i]) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (jumpChainChanged) {
|
||||||
|
final ok = await context.showRoundDialog<bool>(
|
||||||
|
title: libL10n.attention,
|
||||||
|
child: Text(libL10n.askContinue('${l10n.jumpServer} ${libL10n.setting}')),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
if (ok != true) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||||
final ok = await context.showRoundDialog<bool>(
|
final ok = await context.showRoundDialog<bool>(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
@@ -277,7 +301,8 @@ extension _Actions on _ServerEditPageState {
|
|||||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||||
autoConnect: _autoConnect.value,
|
autoConnect: _autoConnect.value,
|
||||||
jumpId: _jumpServer.value,
|
jumpId: null,
|
||||||
|
jumpChainIds: _jumpChain.value.isEmpty ? null : _jumpChain.value,
|
||||||
custom: custom,
|
custom: custom,
|
||||||
wolCfg: wol,
|
wolCfg: wol,
|
||||||
envs: _env.value.isEmpty ? null : _env.value,
|
envs: _env.value.isEmpty ? null : _env.value,
|
||||||
@@ -421,7 +446,7 @@ extension _Utils on _ServerEditPageState {
|
|||||||
|
|
||||||
_altUrlController.text = spi.alterUrl ?? '';
|
_altUrlController.text = spi.alterUrl ?? '';
|
||||||
_autoConnect.value = spi.autoConnect;
|
_autoConnect.value = spi.autoConnect;
|
||||||
_jumpServer.value = spi.jumpId;
|
_jumpChain.value = spi.jumpChainIds ?? (spi.jumpId == null ? const <String>[] : [spi.jumpId!]);
|
||||||
|
|
||||||
final custom = spi.custom;
|
final custom = spi.custom;
|
||||||
if (custom != null) {
|
if (custom != null) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:server_box/view/page/private_key/edit.dart';
|
|||||||
import 'package:server_box/view/page/server/discovery/discovery.dart';
|
import 'package:server_box/view/page/server/discovery/discovery.dart';
|
||||||
|
|
||||||
part 'actions.dart';
|
part 'actions.dart';
|
||||||
|
part 'jump_chain.dart';
|
||||||
part 'widget.dart';
|
part 'widget.dart';
|
||||||
|
|
||||||
class ServerEditPage extends ConsumerStatefulWidget {
|
class ServerEditPage extends ConsumerStatefulWidget {
|
||||||
@@ -66,7 +67,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
/// -1: non selected, null: password, others: index of private key
|
/// -1: non selected, null: password, others: index of private key
|
||||||
final _keyIdx = ValueNotifier<int?>(null);
|
final _keyIdx = ValueNotifier<int?>(null);
|
||||||
final _autoConnect = ValueNotifier(true);
|
final _autoConnect = ValueNotifier(true);
|
||||||
final _jumpServer = nvn<String?>();
|
final _jumpChain = <String>[].vn;
|
||||||
final _pveIgnoreCert = ValueNotifier(false);
|
final _pveIgnoreCert = ValueNotifier(false);
|
||||||
final _env = <String, String>{}.vn;
|
final _env = <String, String>{}.vn;
|
||||||
final _customCmds = <String, String>{}.vn;
|
final _customCmds = <String, String>{}.vn;
|
||||||
@@ -100,7 +101,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
|
|
||||||
_keyIdx.dispose();
|
_keyIdx.dispose();
|
||||||
_autoConnect.dispose();
|
_autoConnect.dispose();
|
||||||
_jumpServer.dispose();
|
_jumpChain.dispose();
|
||||||
_pveIgnoreCert.dispose();
|
_pveIgnoreCert.dispose();
|
||||||
_env.dispose();
|
_env.dispose();
|
||||||
_customCmds.dispose();
|
_customCmds.dispose();
|
||||||
@@ -199,7 +200,6 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
),
|
),
|
||||||
_buildAuth(),
|
_buildAuth(),
|
||||||
_buildSystemType(),
|
_buildSystemType(),
|
||||||
_buildJumpServer(),
|
|
||||||
_buildMore(),
|
_buildMore(),
|
||||||
];
|
];
|
||||||
return AutoMultiList(children: children);
|
return AutoMultiList(children: children);
|
||||||
|
|||||||
176
lib/view/page/server/edit/jump_chain.dart
Normal file
176
lib/view/page/server/edit/jump_chain.dart
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
part of 'edit.dart';
|
||||||
|
|
||||||
|
extension _JumpChain on _ServerEditPageState {
|
||||||
|
Widget _buildJumpChain() {
|
||||||
|
final serversState = ref.watch(serversProvider);
|
||||||
|
final servers = serversState.servers;
|
||||||
|
final selfId = spi?.id;
|
||||||
|
|
||||||
|
if (selfId == null) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.map),
|
||||||
|
title: Text(l10n.jumpServer),
|
||||||
|
subtitle: Text(libL10n.empty, style: UIs.textGrey),
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
String serverNameOrId(String id) {
|
||||||
|
return servers[id]?.name ?? id;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> flattenHopIds(String id, {required Set<String> visited}) {
|
||||||
|
if (!visited.add(id)) return const <String>[];
|
||||||
|
final spi = servers[id];
|
||||||
|
if (spi == null) return const <String>[];
|
||||||
|
|
||||||
|
final hops = spi.jumpChainIds;
|
||||||
|
if (hops == null || hops.isEmpty) return const <String>[];
|
||||||
|
|
||||||
|
final flat = <String>[];
|
||||||
|
for (final hopId in hops) {
|
||||||
|
flat.add(hopId);
|
||||||
|
flat.addAll(flattenHopIds(hopId, visited: visited));
|
||||||
|
}
|
||||||
|
return flat;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool containsCycleWithCandidate(String candidateId) {
|
||||||
|
final queue = [..._jumpChain.value, candidateId];
|
||||||
|
|
||||||
|
final directVisited = <String>{selfId};
|
||||||
|
for (final hopId in queue) {
|
||||||
|
if (hopId == selfId) return true;
|
||||||
|
if (!directVisited.add(hopId)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final hopId in queue) {
|
||||||
|
final extra = flattenHopIds(hopId, visited: <String>{selfId});
|
||||||
|
for (final id in extra) {
|
||||||
|
if (id == selfId) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? buildTextNearToFar() {
|
||||||
|
if (_jumpChain.value.isEmpty) return null;
|
||||||
|
final flat = <String>[];
|
||||||
|
final visited = <String>{selfId};
|
||||||
|
for (final hopId in _jumpChain.value) {
|
||||||
|
flat.add(hopId);
|
||||||
|
flat.addAll(flattenHopIds(hopId, visited: visited));
|
||||||
|
}
|
||||||
|
final names = flat.map(serverNameOrId).toList();
|
||||||
|
if (names.isEmpty) return null;
|
||||||
|
return names.join(' → ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String? buildTextFarToNear() {
|
||||||
|
final text = buildTextNearToFar();
|
||||||
|
if (text == null) return null;
|
||||||
|
return text.split(' → ').reversed.join(' → ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _jumpChain.listenVal((_) {
|
||||||
|
final nearToFar2 = buildTextNearToFar();
|
||||||
|
final farToNear2 = buildTextFarToNear();
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.map),
|
||||||
|
title: Text(l10n.jumpServer),
|
||||||
|
subtitle: (nearToFar2 == null)
|
||||||
|
? Text(libL10n.empty, style: UIs.textGrey)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('${l10n.route}: $nearToFar2', style: UIs.textGrey),
|
||||||
|
Text('${libL10n.path}: $farToNear2', style: UIs.textGrey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: () async {
|
||||||
|
if (serversState.serverOrder.isEmpty) {
|
||||||
|
context.showSnackBar(libL10n.empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final candidates = serversState.serverOrder.where((e) => e != selfId).toList();
|
||||||
|
if (candidates.isEmpty) {
|
||||||
|
context.showSnackBar(libL10n.empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a hop
|
||||||
|
final nextHop = await context.showPickSingleDialog<String>(
|
||||||
|
title: '${l10n.jumpServer} (+1)',
|
||||||
|
items: candidates.where((id) => !containsCycleWithCandidate(id)).toList(),
|
||||||
|
display: serverNameOrId,
|
||||||
|
clearable: true,
|
||||||
|
);
|
||||||
|
if (nextHop == null) return;
|
||||||
|
|
||||||
|
_jumpChain.value = [..._jumpChain.value, nextHop];
|
||||||
|
|
||||||
|
// If user wants to manage order/remove, offer a simple editor dialog
|
||||||
|
await context.showRoundDialog<void>(
|
||||||
|
title: l10n.jumpServer,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 320,
|
||||||
|
child: _jumpChain.listenVal((hops) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: hops.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final id = hops[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(serverNameOrId(id)),
|
||||||
|
subtitle: Text(id, style: UIs.textGrey),
|
||||||
|
trailing: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_upward, size: 18),
|
||||||
|
onPressed: index == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final list = [..._jumpChain.value];
|
||||||
|
final tmp = list[index - 1];
|
||||||
|
list[index - 1] = list[index];
|
||||||
|
list[index] = tmp;
|
||||||
|
_jumpChain.value = list;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_downward, size: 18),
|
||||||
|
onPressed: index == hops.length - 1
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final list = [..._jumpChain.value];
|
||||||
|
final tmp = list[index + 1];
|
||||||
|
list[index + 1] = list[index];
|
||||||
|
list[index] = tmp;
|
||||||
|
_jumpChain.value = list;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete, size: 18),
|
||||||
|
onPressed: () {
|
||||||
|
final list = [..._jumpChain.value]..removeAt(index);
|
||||||
|
_jumpChain.value = list;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
actions: Btnx.oks,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).cardx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,6 +132,7 @@ extension _Widgets on _ServerEditPageState {
|
|||||||
return ExpandTile(
|
return ExpandTile(
|
||||||
title: Text(l10n.more),
|
title: Text(l10n.more),
|
||||||
children: [
|
children: [
|
||||||
|
_buildJumpChain(),
|
||||||
Input(
|
Input(
|
||||||
controller: _logoUrlCtrl,
|
controller: _logoUrlCtrl,
|
||||||
type: TextInputType.url,
|
type: TextInputType.url,
|
||||||
@@ -347,48 +348,6 @@ extension _Widgets on _ServerEditPageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildJumpServer() {
|
|
||||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
|
||||||
final srvs = ref
|
|
||||||
.watch(serversProvider)
|
|
||||||
.servers
|
|
||||||
.values
|
|
||||||
.where((e) => e.jumpId == null)
|
|
||||||
.where((e) => e.id != spi?.id)
|
|
||||||
.toList();
|
|
||||||
final choice = _jumpServer.listenVal((val) {
|
|
||||||
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
|
||||||
return Choice<Spi>(
|
|
||||||
multiple: false,
|
|
||||||
clearable: true,
|
|
||||||
value: srv != null ? [srv] : [],
|
|
||||||
builder: (state, _) => Wrap(
|
|
||||||
children: List<Widget>.generate(srvs.length, (index) {
|
|
||||||
final item = srvs[index];
|
|
||||||
return ChoiceChipX<Spi>(
|
|
||||||
label: item.name,
|
|
||||||
state: state,
|
|
||||||
value: item,
|
|
||||||
onSelected: (srv, on) {
|
|
||||||
if (on) {
|
|
||||||
_jumpServer.value = srv.id;
|
|
||||||
} else {
|
|
||||||
_jumpServer.value = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return ExpandTile(
|
|
||||||
leading: const Icon(Icons.map),
|
|
||||||
initiallyExpanded: _jumpServer.value != null,
|
|
||||||
childrenPadding: padding,
|
|
||||||
title: Text(l10n.jumpServer),
|
|
||||||
children: [choice],
|
|
||||||
).cardx;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildWriteScriptTip() {
|
Widget _buildWriteScriptTip() {
|
||||||
return Btn.tile(
|
return Btn.tile(
|
||||||
|
|||||||
@@ -346,6 +346,6 @@ class _ServerPageState extends ConsumerState<ServerPage>
|
|||||||
|
|
||||||
static const _kCardHeightMin = 23.0;
|
static const _kCardHeightMin = 23.0;
|
||||||
static const _kCardHeightFlip = 99.0;
|
static const _kCardHeightFlip = 99.0;
|
||||||
static const _kCardHeightNormal = 108.0;
|
static const _kCardHeightNormal = 110.0;
|
||||||
static const _kCardHeightMoveOutFuncs = 135.0;
|
static const _kCardHeightMoveOutFuncs = 135.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,19 +94,33 @@ extension _App on _AppSettingsPageState {
|
|||||||
}),
|
}),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
withTextFieldController((ctrl) async {
|
withTextFieldController((ctrl) async {
|
||||||
|
ctrl.text = Color(_setting.colorSeed.fetch()).toHex;
|
||||||
await context.showRoundDialog(
|
await context.showRoundDialog(
|
||||||
title: libL10n.primaryColorSeed,
|
title: libL10n.primaryColorSeed,
|
||||||
child: StatefulBuilder(
|
child: StatefulBuilder(
|
||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
/// Plugin [dynamic_color] is not supported on iOS
|
|
||||||
if (!isIOS)
|
if (!isIOS)
|
||||||
ListTile(
|
DynamicColorBuilder(
|
||||||
title: Text(l10n.followSystem),
|
builder: (light, dark) {
|
||||||
trailing: StoreSwitch(
|
final supported = light != null || dark != null;
|
||||||
prop: _setting.useSystemPrimaryColor,
|
if (!supported) {
|
||||||
callback: (_) => setState(() {}),
|
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||||
),
|
_setting.useSystemPrimaryColor.put(false);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
title: Text(l10n.followSystem),
|
||||||
|
trailing: StoreSwitch(
|
||||||
|
prop: _setting.useSystemPrimaryColor,
|
||||||
|
callback: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (!_setting.useSystemPrimaryColor.fetch()) {
|
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||||
@@ -129,12 +143,22 @@ extension _App on _AppSettingsPageState {
|
|||||||
|
|
||||||
void _onSaveColor(String s) {
|
void _onSaveColor(String s) {
|
||||||
final color = s.fromColorHex;
|
final color = s.fromColorHex;
|
||||||
|
|
||||||
if (color == null) {
|
if (color == null) {
|
||||||
context.showSnackBar(libL10n.fail);
|
context.showSnackBar(libL10n.fail);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UIs.colorSeed = color;
|
|
||||||
|
// Save the color seed to settings
|
||||||
_setting.colorSeed.put(color.value255);
|
_setting.colorSeed.put(color.value255);
|
||||||
|
|
||||||
|
// Only update UIs colors if we're not in system mode
|
||||||
|
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||||
|
UIs.primaryColor = color;
|
||||||
|
UIs.colorSeed = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
RNodes.app.notify();
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_highlight/theme_map.dart';
|
import 'package:flutter_highlight/theme_map.dart';
|
||||||
@@ -28,6 +29,7 @@ import 'package:server_box/view/page/setting/seq/srv_seq.dart';
|
|||||||
import 'package:server_box/view/page/setting/seq/virt_key.dart';
|
import 'package:server_box/view/page/setting/seq/virt_key.dart';
|
||||||
|
|
||||||
part 'about.dart';
|
part 'about.dart';
|
||||||
|
part 'entries/ai.dart';
|
||||||
part 'entries/app.dart';
|
part 'entries/app.dart';
|
||||||
part 'entries/container.dart';
|
part 'entries/container.dart';
|
||||||
part 'entries/editor.dart';
|
part 'entries/editor.dart';
|
||||||
@@ -35,7 +37,6 @@ part 'entries/full_screen.dart';
|
|||||||
part 'entries/server.dart';
|
part 'entries/server.dart';
|
||||||
part 'entries/sftp.dart';
|
part 'entries/sftp.dart';
|
||||||
part 'entries/ssh.dart';
|
part 'entries/ssh.dart';
|
||||||
part 'entries/ai.dart';
|
|
||||||
|
|
||||||
const _kIconSize = 23.0;
|
const _kIconSize = 23.0;
|
||||||
|
|
||||||
@@ -71,9 +72,9 @@ class _SettingsPageState extends ConsumerState<SettingsPage> with SingleTickerPr
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Btn.text(
|
Btn.text(
|
||||||
text: 'Logs',
|
text: context.l10n.logs,
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
DebugPage.route.go(context, args: const DebugPageArgs(title: 'Logs(${BuildData.build})')),
|
DebugPage.route.go(context, args: DebugPageArgs(title: '${context.l10n.logs}(${BuildData.build})')),
|
||||||
),
|
),
|
||||||
Btn.icon(
|
Btn.icon(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class _SnippetListPageState extends ConsumerState<SnippetListPage> with Automati
|
|||||||
tags: snippetState.tags.vn,
|
tags: snippetState.tags.vn,
|
||||||
onTagChanged: (tag) => _tag.value = tag,
|
onTagChanged: (tag) => _tag.value = tag,
|
||||||
initTag: _tag.value,
|
initTag: _tag.value,
|
||||||
|
singleLine: true,
|
||||||
),
|
),
|
||||||
body: _buildSnippetList(snippets, tag),
|
body: _buildSnippetList(snippets, tag),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
|
|||||||
String? _streamingContent;
|
String? _streamingContent;
|
||||||
String? _error;
|
String? _error;
|
||||||
bool _isStreaming = false;
|
bool _isStreaming = false;
|
||||||
|
bool _isMinimized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -250,6 +251,13 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
|
|||||||
context.showSnackBar(libL10n.success);
|
context.showSnackBar(libL10n.success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _copyText(BuildContext context, String text) async {
|
||||||
|
if (text.trim().isEmpty) return;
|
||||||
|
await Clipboard.setData(ClipboardData(text: text));
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackBar(libL10n.success);
|
||||||
|
}
|
||||||
|
|
||||||
void _sendMessage() {
|
void _sendMessage() {
|
||||||
if (_isStreaming) return;
|
if (_isStreaming) return;
|
||||||
final text = _inputController.text.trim();
|
final text = _inputController.text.trim();
|
||||||
@@ -310,7 +318,23 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
|
|||||||
streaming ? l10n.askAiAwaitingResponse : l10n.askAiNoResponse,
|
streaming ? l10n.askAiAwaitingResponse : l10n.askAiNoResponse,
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
)
|
)
|
||||||
: SimpleMarkdown(data: content);
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SimpleMarkdown(data: content),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => _copyText(context, content),
|
||||||
|
icon: const Icon(Icons.copy, size: 18),
|
||||||
|
label: Text(libL10n.copy),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: CardX(
|
child: CardX(
|
||||||
@@ -364,12 +388,23 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final bottomPadding = MediaQuery.viewInsetsOf(context).bottom;
|
final bottomPadding = MediaQuery.viewInsetsOf(context).bottom;
|
||||||
|
final heightFactor = _isMinimized ? 0.18 : 0.85;
|
||||||
|
|
||||||
return FractionallySizedBox(
|
return TweenAnimationBuilder<double>(
|
||||||
heightFactor: 0.85,
|
tween: Tween<double>(end: heightFactor),
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
builder: (context, animatedHeightFactor, child) {
|
||||||
|
return ClipRect(
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
heightFactor: animatedHeightFactor,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -379,83 +414,96 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
|
|||||||
if (_isStreaming)
|
if (_isStreaming)
|
||||||
const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_isMinimized ? Icons.unfold_more : Icons.unfold_less),
|
||||||
|
tooltip: libL10n.fold,
|
||||||
|
onPressed: () {
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
setState(() {
|
||||||
|
_isMinimized = !_isMinimized;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()),
|
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
if (!_isMinimized) ...[
|
||||||
child: Scrollbar(
|
Expanded(
|
||||||
controller: _scrollController,
|
child: Scrollbar(
|
||||||
child: ListView(
|
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
child: ListView(
|
||||||
children: [
|
controller: _scrollController,
|
||||||
Text(context.l10n.askAiSelectedContent, style: theme.textTheme.titleMedium),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||||
const SizedBox(height: 6),
|
children: [
|
||||||
CardX(
|
Text(context.l10n.askAiSelectedContent, style: theme.textTheme.titleMedium),
|
||||||
child: Padding(
|
const SizedBox(height: 6),
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: SelectableText(
|
|
||||||
widget.selection,
|
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(context.l10n.askAiConversation, style: theme.textTheme.titleMedium),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
..._buildConversationWidgets(context, theme),
|
|
||||||
if (_error != null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
CardX(
|
CardX(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
child: SelectableText(
|
||||||
|
widget.selection,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(context.l10n.askAiConversation, style: theme.textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
..._buildConversationWidgets(context, theme),
|
||||||
|
if (_error != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
CardX(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (_isStreaming) ...[const SizedBox(height: 16), const LinearProgressIndicator()],
|
||||||
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
if (_isStreaming) ...[const SizedBox(height: 16), const LinearProgressIndicator()],
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
child: Text(
|
||||||
child: Text(
|
context.l10n.askAiDisclaimer,
|
||||||
context.l10n.askAiDisclaimer,
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
color: theme.colorScheme.error,
|
||||||
color: theme.colorScheme.error,
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomPadding),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Input(
|
|
||||||
controller: _inputController,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 4,
|
|
||||||
hint: context.l10n.askAiFollowUpHint,
|
|
||||||
action: TextInputAction.send,
|
|
||||||
onSubmitted: (_) => _sendMessage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Btn.icon(
|
|
||||||
onTap: _isStreaming || _inputController.text.trim().isEmpty ? null : _sendMessage,
|
|
||||||
icon: const Icon(Icons.send, size: 18),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomPadding),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Input(
|
||||||
|
controller: _inputController,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 4,
|
||||||
|
hint: context.l10n.askAiFollowUpHint,
|
||||||
|
action: TextInputAction.send,
|
||||||
|
onSubmitted: (_) => _sendMessage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Btn.icon(
|
||||||
|
onTap: _isStreaming || _inputController.text.trim().isEmpty ? null : _sendMessage,
|
||||||
|
icon: const Icon(Icons.send, size: 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).cardx,
|
||||||
|
),
|
||||||
|
] else
|
||||||
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
).cardx,
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class SSHPageState extends ConsumerState<SSHPage>
|
|||||||
deleteDetection: isMobile,
|
deleteDetection: isMobile,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
|
keyboardAppearance: _isDark ? Brightness.dark : Brightness.light,
|
||||||
showToolbar: isMobile,
|
showToolbar: true,
|
||||||
viewOffset: Offset(2 * _horizonPadding, CustomAppBar.sysStatusBarHeight),
|
viewOffset: Offset(2 * _horizonPadding, CustomAppBar.sysStatusBarHeight),
|
||||||
hideScrollBar: false,
|
hideScrollBar: false,
|
||||||
focusNode: widget.args.focusNode,
|
focusNode: widget.args.focusNode,
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class _LocalFilePageState extends ConsumerState<LocalFilePage> with AutomaticKee
|
|||||||
|
|
||||||
final item = items![index];
|
final item = items![index];
|
||||||
final file = item.$1;
|
final file = item.$1;
|
||||||
final fileName = file.path.split('/').last;
|
final fileName = file.path.split(Pfs.seperator).last;
|
||||||
final stat = item.$2;
|
final stat = item.$2;
|
||||||
final isDir = stat.type == FileSystemEntityType.directory;
|
final isDir = stat.type == FileSystemEntityType.directory;
|
||||||
|
|
||||||
@@ -140,11 +140,23 @@ class _LocalFilePageState extends ConsumerState<LocalFilePage> with AutomaticKee
|
|||||||
required FileStat stat,
|
required FileStat stat,
|
||||||
required bool isDir,
|
required bool isDir,
|
||||||
}) {
|
}) {
|
||||||
|
final isServerFolder = isDir && file.parent.path == Paths.file;
|
||||||
|
String? serverName;
|
||||||
|
if (isServerFolder) {
|
||||||
|
final servers = ref.read(serversProvider).servers;
|
||||||
|
final server = servers[fileName];
|
||||||
|
if (server != null) {
|
||||||
|
serverName = server.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CardX(
|
return CardX(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),
|
leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),
|
||||||
title: Text(fileName),
|
title: Text(serverName ?? fileName),
|
||||||
subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
subtitle: isDir
|
||||||
|
? (serverName != null ? Text(fileName, style: UIs.textGrey) : null)
|
||||||
|
: Text(stat.size.bytes2Str, style: UIs.textGrey),
|
||||||
trailing: Text(stat.modified.ymdhms(), style: UIs.textGrey),
|
trailing: Text(stat.modified.ymdhms(), style: UIs.textGrey),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (isDir) {
|
if (isDir) {
|
||||||
@@ -216,7 +228,7 @@ extension _Actions on _LocalFilePageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showFileActionDialog(FileSystemEntity file) async {
|
Future<void> _showFileActionDialog(FileSystemEntity file) async {
|
||||||
final fileName = file.path.split('/').lastOrNull ?? '';
|
final fileName = file.path.split(Pfs.seperator).lastOrNull ?? '';
|
||||||
if (isPickFile) {
|
if (isPickFile) {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.file,
|
title: libL10n.file,
|
||||||
@@ -308,7 +320,7 @@ extension _Actions on _LocalFilePageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteDialog(FileSystemEntity file) {
|
void _showDeleteDialog(FileSystemEntity file) {
|
||||||
final fileName = file.path.split('/').last;
|
final fileName = file.path.split(Pfs.seperator).last;
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.delete,
|
title: libL10n.delete,
|
||||||
child: Text(libL10n.askContinue('${libL10n.delete} $fileName')),
|
child: Text(libL10n.askContinue('${libL10n.delete} $fileName')),
|
||||||
|
|||||||
@@ -177,32 +177,56 @@ extension _UI on _SftpPageState {
|
|||||||
|
|
||||||
Widget _buildItem(SftpName file, {VoidCallback? beforeTap}) {
|
Widget _buildItem(SftpName file, {VoidCallback? beforeTap}) {
|
||||||
final isDir = file.attr.isDirectory;
|
final isDir = file.attr.isDirectory;
|
||||||
final trailing = Text(
|
final double screenWidth = MediaQuery.sizeOf(context).width;
|
||||||
'${_getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}',
|
if (screenWidth < 350) {
|
||||||
style: UIs.textGrey,
|
return CardX(
|
||||||
textAlign: TextAlign.right,
|
child: ListTile(
|
||||||
);
|
leading: Icon(isDir ? Icons.folder_outlined : Icons.insert_drive_file),
|
||||||
return CardX(
|
title: Text(file.filename),
|
||||||
child: ListTile(
|
subtitle: isDir ? Text('${_getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}', style: UIs.textGrey) :
|
||||||
leading: Icon(isDir ? Icons.folder_outlined : Icons.insert_drive_file),
|
Text('${(file.attr.size ?? 0).bytes2Str}\n${_getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}', style: UIs.textGrey),
|
||||||
title: Text(file.filename),
|
onTap: () {
|
||||||
trailing: trailing,
|
beforeTap?.call();
|
||||||
subtitle: isDir ? null : Text((file.attr.size ?? 0).bytes2Str, style: UIs.textGrey),
|
if (isDir) {
|
||||||
onTap: () {
|
_status.path.path = file.filename;
|
||||||
beforeTap?.call();
|
_listDir();
|
||||||
if (isDir) {
|
} else {
|
||||||
_status.path.path = file.filename;
|
_onItemPress(file, true);
|
||||||
_listDir();
|
}
|
||||||
} else {
|
},
|
||||||
_onItemPress(file, true);
|
onLongPress: () {
|
||||||
}
|
beforeTap?.call();
|
||||||
},
|
_onItemPress(file, !isDir);
|
||||||
onLongPress: () {
|
},
|
||||||
beforeTap?.call();
|
),
|
||||||
_onItemPress(file, !isDir);
|
);
|
||||||
},
|
} else {
|
||||||
),
|
return CardX(
|
||||||
);
|
child: ListTile(
|
||||||
|
leading: Icon(isDir ? Icons.folder_outlined : Icons.insert_drive_file),
|
||||||
|
title: Text(file.filename),
|
||||||
|
trailing: Text(
|
||||||
|
'${_getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}',
|
||||||
|
style: UIs.textGrey,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
subtitle: isDir ? null : Text((file.attr.size ?? 0).bytes2Str, style: UIs.textGrey),
|
||||||
|
onTap: () {
|
||||||
|
beforeTap?.call();
|
||||||
|
if (isDir) {
|
||||||
|
_status.path.path = file.filename;
|
||||||
|
_listDir();
|
||||||
|
} else {
|
||||||
|
_onItemPress(file, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
beforeTap?.call();
|
||||||
|
_onItemPress(file, !isDir);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +585,11 @@ extension _Actions on _SftpPageState {
|
|||||||
|
|
||||||
/// Local file dir + server id + remote path
|
/// Local file dir + server id + remote path
|
||||||
String _getLocalPath(String remotePath) {
|
String _getLocalPath(String remotePath) {
|
||||||
return Paths.file.joinPath(widget.args.spi.oldId).joinPath(remotePath);
|
var normalizedPath = remotePath.replaceAll('/', Pfs.seperator);
|
||||||
|
if (normalizedPath.startsWith(Pfs.seperator)) {
|
||||||
|
normalizedPath = normalizedPath.substring(1);
|
||||||
|
}
|
||||||
|
return Paths.file.joinPath(widget.args.spi.id).joinPath(normalizedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Only return true if the path is changed
|
/// Only return true if the path is changed
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class _SftpMissionPageState extends ConsumerState<SftpMissionPage> {
|
|||||||
return switch (status.status) {
|
return switch (status.status) {
|
||||||
const (SftpWorkerStatus.finished) => _buildFinished(status),
|
const (SftpWorkerStatus.finished) => _buildFinished(status),
|
||||||
const (SftpWorkerStatus.loading) => _buildLoading(status),
|
const (SftpWorkerStatus.loading) => _buildLoading(status),
|
||||||
const (SftpWorkerStatus.sshConnectted) => _buildConnected(status),
|
const (SftpWorkerStatus.sshConnected) => _buildConnected(status),
|
||||||
const (SftpWorkerStatus.preparing) => _buildPreparing(status),
|
const (SftpWorkerStatus.preparing) => _buildPreparing(status),
|
||||||
_ => _buildDefault(status),
|
_ => _buildDefault(status),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -471,7 +471,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||||
@@ -481,7 +481,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "Server Box";
|
PRODUCT_NAME = "Server Box";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -608,7 +608,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||||
@@ -618,7 +618,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "Server Box";
|
PRODUCT_NAME = "Server Box";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -638,7 +638,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1276;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -649,7 +649,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MARKETING_VERSION = 1.0.1276;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "Server Box";
|
PRODUCT_NAME = "Server Box";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@@ -10,4 +10,19 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
if let controller = mainFlutterWindow?.contentViewController as? FlutterViewController {
|
||||||
|
let channel = FlutterMethodChannel(name: "about", binaryMessenger: controller.engine.binaryMessenger)
|
||||||
|
channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
|
||||||
|
if call.method == "showAboutPanel" {
|
||||||
|
NSApp.orderFrontStandardAboutPanel(nil)
|
||||||
|
result(nil)
|
||||||
|
} else {
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.applicationDidFinishLaunching(notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
pubspec.lock
30
pubspec.lock
@@ -205,10 +205,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: camera_web
|
name: camera_web
|
||||||
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
|
sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5"
|
version: "0.3.5+3"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -440,10 +440,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.5"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -472,8 +472,8 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.52"
|
ref: "v1.0.53"
|
||||||
resolved-ref: "38e7d41ccd71bf44e286d86b4ad656f05c5c2548"
|
resolved-ref: "61ee37ea6f082592f5be56340b7746dce4ffbfda"
|
||||||
url: "https://github.com/lppcg/fl_build.git"
|
url: "https://github.com/lppcg/fl_build.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
@@ -727,18 +727,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hive_ce
|
name: hive_ce
|
||||||
sha256: "81d39a03c4c0ba5938260a8c3547d2e71af59defecea21793d57fc3551f0d230"
|
sha256: "29f8791bf13fa6cf7435a58f1f82a7c9706973c867affa77c34d91e105762664"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.15.1"
|
version: "2.17.0"
|
||||||
hive_ce_flutter:
|
hive_ce_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hive_ce_flutter
|
name: hive_ce_flutter
|
||||||
sha256: "26d656c9e8974f0732f1d09020e2d7b08ba841b8961a02dbfb6caf01474b0e9a"
|
sha256: "2677e95a333ff15af43ccd06af7eb7abbf1a4f154ea071997f3de4346cae913a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.3"
|
version: "2.3.4"
|
||||||
hive_ce_generator:
|
hive_ce_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -831,10 +831,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: isolate_channel
|
name: isolate_channel
|
||||||
sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d
|
sha256: "68191008e3a219bc87cc8cddbcd1e29810bd9f3a0fdc2108b574ccbd9aafda08"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2+1"
|
version: "0.3.0"
|
||||||
isolate_contactor:
|
isolate_contactor:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1602,7 +1602,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.2"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
@@ -1750,10 +1750,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
|
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.4"
|
version: "1.2.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: server_box
|
name: server_box
|
||||||
description: server status & toolbox app.
|
description: server status & toolbox app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.0.1276+1276
|
version: 1.0.1291+1291
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.9.0"
|
sdk: ">=3.9.0"
|
||||||
@@ -38,6 +38,7 @@ dependencies:
|
|||||||
wake_on_lan: ^4.1.1+3
|
wake_on_lan: ^4.1.1+3
|
||||||
webdav_client_plus: ^1.0.2
|
webdav_client_plus: ^1.0.2
|
||||||
xml: ^6.4.2 # for parsing nvidia-smi
|
xml: ^6.4.2 # for parsing nvidia-smi
|
||||||
|
url_launcher: ^6.2.6
|
||||||
dartssh2:
|
dartssh2:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/dartssh2
|
url: https://github.com/lollipopkit/dartssh2
|
||||||
@@ -102,7 +103,7 @@ dev_dependencies:
|
|||||||
fl_build:
|
fl_build:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lppcg/fl_build.git
|
url: https://github.com/lppcg/fl_build.git
|
||||||
ref: v1.0.52
|
ref: v1.0.53
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
generate: true
|
generate: true
|
||||||
|
|||||||
93
test/jump_server_test.dart
Normal file
93
test/jump_server_test.dart
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:server_box/core/utils/server.dart';
|
||||||
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Jump server', () {
|
||||||
|
test('resolveMergedJumpChain throws when injected chain misses jump server', () {
|
||||||
|
const spi = Spi(
|
||||||
|
name: 'target',
|
||||||
|
ip: '10.0.0.10',
|
||||||
|
port: 22,
|
||||||
|
user: 'root',
|
||||||
|
id: 't',
|
||||||
|
jumpId: 'missing',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => resolveMergedJumpChain(spi, jumpChain: const <Spi>[]),
|
||||||
|
throwsA(
|
||||||
|
isA<SSHErr>().having(
|
||||||
|
(e) => e.type,
|
||||||
|
'type',
|
||||||
|
SSHErrType.connect,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveMergedJumpChain merges and dedups', () {
|
||||||
|
const c = Spi(name: 'c', ip: '10.0.0.30', port: 22, user: 'root', id: 'c');
|
||||||
|
const d = Spi(name: 'd', ip: '10.0.0.40', port: 22, user: 'root', id: 'd');
|
||||||
|
const b = Spi(
|
||||||
|
name: 'b',
|
||||||
|
ip: '10.0.0.20',
|
||||||
|
port: 22,
|
||||||
|
user: 'root',
|
||||||
|
id: 'b',
|
||||||
|
jumpChainIds: ['c', 'd'],
|
||||||
|
);
|
||||||
|
const target = Spi(
|
||||||
|
name: 'target',
|
||||||
|
ip: '10.0.0.10',
|
||||||
|
port: 22,
|
||||||
|
user: 'root',
|
||||||
|
id: 't',
|
||||||
|
jumpChainIds: ['b', 'c'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final chain = resolveMergedJumpChain(target, jumpChain: const <Spi>[b, c, d]);
|
||||||
|
expect(chain.map((e) => e.id).toList(), ['c', 'd', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveMergedJumpChain detects jump loop', () {
|
||||||
|
const b = Spi(
|
||||||
|
name: 'b',
|
||||||
|
ip: '10.0.0.20',
|
||||||
|
port: 22,
|
||||||
|
user: 'root',
|
||||||
|
id: 'b',
|
||||||
|
jumpChainIds: ['c'],
|
||||||
|
);
|
||||||
|
const c = Spi(
|
||||||
|
name: 'c',
|
||||||
|
ip: '10.0.0.30',
|
||||||
|
port: 22,
|
||||||
|
user: 'root',
|
||||||
|
id: 'c',
|
||||||
|
jumpChainIds: ['b'],
|
||||||
|
);
|
||||||
|
const target = Spi(
|
||||||
|
name: 'target',
|
||||||
|
ip: '10.0.0.10',
|
||||||
|
port: 22,
|
||||||
|
user: 'root',
|
||||||
|
id: 't',
|
||||||
|
jumpChainIds: ['b'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => resolveMergedJumpChain(target, jumpChain: const <Spi>[b, c]),
|
||||||
|
throwsA(
|
||||||
|
isA<SSHErr>().having(
|
||||||
|
(e) => e.type,
|
||||||
|
'type',
|
||||||
|
SSHErrType.connect,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user