Compare commits
59 Commits
dev
..
c87d5d4d38
| Author | SHA1 | Date | |
|---|---|---|---|
| c87d5d4d38 | |||
| ce4abb7e01 | |||
| dbcdf8b8a4 | |||
| e82480b937 | |||
| 8e27ddeded | |||
| 961d419c4d | |||
| aa0fd94cb2 | |||
| 86687cac58 | |||
| 1b1b38aba5 | |||
| beae70edb7 | |||
| d2ea8436f9 | |||
| 95f8f3afbe | |||
| a17fa079bc | |||
| 9302764802 | |||
| c058c2cea6 | |||
| 55d7508807 | |||
| adcde623c7 | |||
| 83e742a9dc | |||
| 569f9f6d05 | |||
| 4fdf4da4f3 | |||
| b059d01556 | |||
| 909020654f | |||
| 1809c11473 | |||
| 343bb50c77 | |||
| 11afee1d6f | |||
| 96d1a107cf | |||
| 52f241a5a7 | |||
| 5dfb2b7432 | |||
| 85dc810421 | |||
| ee3fdd91fa | |||
| e537775e71 | |||
| f97a21be32 | |||
| d3f48e345e | |||
| ac8a04fd24 | |||
| becc03acfd | |||
| 9123d5f0e4 | |||
| 91185a593a | |||
| d6386ecfa9 | |||
| 62d1af8517 | |||
| 3b05d4ff72 | |||
| bcb4d17236 | |||
| d99f588a5b | |||
| 691dd6e1d6 | |||
| b442289b8a | |||
| 89d13e6237 | |||
| 8fd742a688 | |||
| 325a63d35f | |||
| 679c303457 | |||
| 59aa67d456 | |||
| 2a1ee3018d | |||
| 76e83e0135 | |||
| 89d6ec3c76 | |||
| 670eb7ebc9 | |||
| 46295f4543 | |||
| a47174df56 | |||
| 748ec6bee9 | |||
| a884e6c838 | |||
| 920b86e56a | |||
| a78b40c339 |
+3
-2
@@ -1,3 +1,4 @@
|
||||
[submodule "libcore"]
|
||||
path = libcore
|
||||
url = https://github.com/hiddify/hiddify-next-core
|
||||
path = libcore
|
||||
url = https://github.com/hiddify/hiddify-next-core
|
||||
branch = main
|
||||
|
||||
@@ -12,6 +12,10 @@ ifeq ($(OS),Windows_NT)
|
||||
endif
|
||||
endif
|
||||
|
||||
APP_NAME=HiFastVPN
|
||||
VERSION_NAME=$(shell grep '^version:' pubspec.yaml | sed 's/version: //;s/+.*//;s/[^0-9.]//g')
|
||||
VERSION_BUILD=$(shell grep '^version:' pubspec.yaml | sed 's/.*+//;s/[^0-9]//g')
|
||||
|
||||
|
||||
BINDIR=libcore$(SEP)bin
|
||||
ANDROID_OUT=android$(SEP)app$(SEP)libs
|
||||
@@ -54,7 +58,7 @@ gen:
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
translate:
|
||||
dart run slang
|
||||
dart run slang --overwrite
|
||||
|
||||
|
||||
|
||||
@@ -152,9 +156,24 @@ gen_translations: #generating missing translations using google translate
|
||||
|
||||
android-release: android-apk-release
|
||||
|
||||
android-rename:
|
||||
@FULL_VER=$(VERSION_NAME).$(VERSION_BUILD); \
|
||||
OUT_DIR=build/app/outputs/flutter-apk; \
|
||||
echo "Renaming APKs in $$OUT_DIR to $(APP_NAME)-$$FULL_VER-<ABI>.apk"; \
|
||||
for SRC in "$$OUT_DIR"/app-*-release.apk; do \
|
||||
if [ -f "$$SRC" ]; then \
|
||||
FILENAME=$$(basename "$$SRC"); \
|
||||
TGT="$$OUT_DIR/$(APP_NAME)-$$FULL_VER-$${FILENAME#app-}"; \
|
||||
echo "Renaming $$SRC -> $$TGT"; \
|
||||
mv -v "$$SRC" "$$TGT"; \
|
||||
else \
|
||||
echo "Skip $$SRC, file not found"; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
android-apk-release:
|
||||
echo flutter build apk --target $(TARGET) $(BUILD_ARGS) --target-platform android-arm,android-arm64,android-x64 --split-per-abi --verbose
|
||||
flutter build apk --target $(TARGET) $(BUILD_ARGS) --target-platform android-arm,android-arm64,android-x64 --verbose
|
||||
flutter build apk --target $(TARGET) $(BUILD_ARGS) --target-platform android-arm,android-arm64,android-x64 --verbose
|
||||
ls -R build/app/outputs
|
||||
|
||||
android-aab-release:
|
||||
@@ -169,7 +188,7 @@ linux-release:
|
||||
flutter_distributor package --flutter-build-args=verbose --platform linux --targets deb,rpm,appimage $(DISTRIBUTOR_ARGS)
|
||||
|
||||
macos-release:
|
||||
flutter_distributor package --platform macos --targets dmg,pkg $(DISTRIBUTOR_ARGS)
|
||||
flutter_distributor package --platform macos --targets dmg $(DISTRIBUTOR_ARGS)
|
||||
|
||||
ios-release: #not tested
|
||||
flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist $(DISTRIBUTOR_ARGS)
|
||||
|
||||
@@ -57,7 +57,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "app.hifastvpn.com"
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 36
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
@@ -105,8 +105,8 @@ android {
|
||||
debugSymbolLevel 'FULL'
|
||||
}
|
||||
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<!-- 如果 targetSdkVersion >= 33 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
|
||||
Executable → Regular
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="38" height="45" viewBox="0 0 38 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24.9375 26.0625H33.75V30.2812H23.75V45H14.6562V30.2812H4.71875V26.0625H13.4688L10.0625 19.25H3.40625V15H8L0.5 0.28125H11.0938L19.4062 19.75L27.3125 0.28125H37.4688L30.3438 15H34.8125V19.25H28.2812L24.9375 26.0625Z" fill="black"/>
|
||||
<svg width="32" height="58" viewBox="0 0 32 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 29.7812C8.02083 28.6146 5.20833 26.9896 3.5625 24.9062C1.9375 22.8229 1.125 20.3125 1.125 17.375C1.125 15.4375 1.4375 13.6771 2.0625 12.0938C2.70833 10.5104 3.60417 9.14583 4.75 8C6.22917 6.52083 7.875 5.5 9.6875 4.9375C10.7917 4.58333 12.4062 4.32292 14.5312 4.15625V0H17.5V4.21875C20.8958 4.48958 23.5625 5.32292 25.5 6.71875C29.0208 8.94792 30.8438 12.5625 30.9688 17.5625H22.75C22.5833 15.7083 22.2708 14.3542 21.8125 13.5C21.0208 12.0208 19.5833 11.2083 17.5 11.0625V22.9688C22.4583 24.6771 25.7917 26.1875 27.5 27.5C30.3125 29.6875 31.7188 32.7708 31.7188 36.75C31.7188 42 29.7917 45.8125 25.9375 48.1875C23.5833 49.6458 20.7917 50.5 17.5625 50.75V57.0312H14.5312V50.8125C10.4271 50.3542 7.36458 49.4583 5.34375 48.125C1.78125 45.7292 0.03125 41.6458 0.09375 35.875H8.53125C8.82292 38.5 9.22917 40.2604 9.75 41.1562C10.5625 42.5521 12.1562 43.4583 14.5312 43.875V30.5312L12 29.7812ZM17.5 31.5625V43.7812C19.1458 43.5729 20.3438 43.1979 21.0938 42.6562C22.4062 41.6979 23.0625 40.0417 23.0625 37.6875C23.0625 35.8958 22.4479 34.4792 21.2188 33.4375C20.4896 32.8333 19.25 32.2083 17.5 31.5625ZM14.5312 22.0938V11.125C12.7188 11.1667 11.375 11.6562 10.5 12.5938C9.625 13.5104 9.1875 14.7708 9.1875 16.375C9.1875 18.125 9.84375 19.5104 11.1562 20.5312C11.8854 21.0938 13.0104 21.6146 14.5312 22.0938Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 343 B After Width: | Height: | Size: 1.4 KiB |
@@ -193,7 +193,7 @@
|
||||
"rewardDetails": "Reward Details >",
|
||||
"steps": "Invitation Steps",
|
||||
"inviteFriend": "Invite Friends",
|
||||
"acceptInvite": "Friends accept invitation\nPlace order and register",
|
||||
"acceptInvite": "Friends accept invitationPlace order and register",
|
||||
"getReward": "Get Reward",
|
||||
"shareLink": "Share Link",
|
||||
"shareQR": "Share QR Code",
|
||||
@@ -398,7 +398,7 @@
|
||||
"content": "Would you like to update now?",
|
||||
"updateNow": "Update Now",
|
||||
"updateLater": "Later",
|
||||
"defaultContent": "1. Optimize app performance\n2. Fix known issues\n3. Improve user experience"
|
||||
"defaultContent": "1. Optimize app performance2. Fix known issues3. Improve user experience"
|
||||
},
|
||||
"country": {
|
||||
"cn": "China",
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
"rewardDetails": "Detalles de recompensa >",
|
||||
"steps": "Pasos de invitación",
|
||||
"inviteFriend": "Invitar amigo",
|
||||
"acceptInvite": "El amigo acepta la invitación\ny se registra",
|
||||
"acceptInvite": "El amigo acepta la invitacióny se registra",
|
||||
"getReward": "Obtener recompensa",
|
||||
"shareLink": "Compartir por enlace",
|
||||
"shareQR": "Compartir por código QR",
|
||||
@@ -404,7 +404,7 @@
|
||||
"content": "¿Actualizar ahora?",
|
||||
"updateNow": "Actualizar ahora",
|
||||
"updateLater": "Más tarde",
|
||||
"defaultContent": "1. Optimización del rendimiento de la aplicación\n2. Corrección de problemas conocidos\n3. Mejora de la experiencia del usuario"
|
||||
"defaultContent": "1. Optimización del rendimiento de la aplicación2. Corrección de problemas conocidos3. Mejora de la experiencia del usuario"
|
||||
},
|
||||
"kr_invite": {
|
||||
"close": "Cerrar",
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
"rewardDetails": "Tasu üksikasjad >",
|
||||
"steps": "Kutse Sammud",
|
||||
"inviteFriend": "Kutsu Sõbrad",
|
||||
"acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru",
|
||||
"acceptInvite": "Sõbrad aktsepteerivad kutsetTee tellimus ja registreeru",
|
||||
"getReward": "Saada Tasu",
|
||||
"shareLink": "Jaga Linki",
|
||||
"shareQR": "Jaga QR-koodi",
|
||||
@@ -395,7 +395,7 @@
|
||||
"content": "Uuendada nüüd?",
|
||||
"updateNow": "Uuenda nüüd",
|
||||
"updateLater": "Hiljem",
|
||||
"defaultContent": "1. Rakenduse jõudluse optimeerimine\n2. Teadaolevate probleemide parandamine\n3. Kasutajamugavuse parandamine"
|
||||
"defaultContent": "1. Rakenduse jõudluse optimeerimine2. Teadaolevate probleemide parandamine3. Kasutajamugavuse parandamine"
|
||||
},
|
||||
"country": {
|
||||
"cn": "Hiina",
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"rewardDetails": "報酬の詳細 >",
|
||||
"steps": "招待の手順",
|
||||
"inviteFriend": "友達を招待",
|
||||
"acceptInvite": "友達が招待を受け入れ\n注文して登録",
|
||||
"acceptInvite": "友達が招待を受け入れ注文して登録",
|
||||
"getReward": "報酬を獲得",
|
||||
"shareLink": "リンクを共有",
|
||||
"shareQR": "QRコードを共有",
|
||||
@@ -412,7 +412,7 @@
|
||||
"content": "今すぐアップデートしますか?",
|
||||
"updateNow": "今すぐアップデート",
|
||||
"updateLater": "後で",
|
||||
"defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上"
|
||||
"defaultContent": "1. アプリのパフォーマンス最適化2. 既知の問題の修正3. ユーザー体験の向上"
|
||||
},
|
||||
"country": {
|
||||
"cn": "中国",
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
"content": "Хотите обновить сейчас?",
|
||||
"updateNow": "Обновить сейчас",
|
||||
"later": "Позже",
|
||||
"defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта"
|
||||
"defaultContent": "1. Оптимизация производительности приложения2. Исправление известных проблем3. Улучшение пользовательского опыта"
|
||||
},
|
||||
"country": {
|
||||
"cn": "Китай",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"home": {
|
||||
"welcome": "欢迎使用 Hi快VPN",
|
||||
"disconnected": "未连接",
|
||||
"connecting": "正在连接",
|
||||
"connecting": "正在连接...",
|
||||
"connected": "已连接",
|
||||
"disconnecting": "正在断开",
|
||||
"currentConnectionTitle": "当前连接",
|
||||
@@ -232,13 +232,13 @@
|
||||
"longestConnection": "最长连接时间",
|
||||
"days": "{days}天",
|
||||
"daysOfWeek": {
|
||||
"monday": "周\n一",
|
||||
"tuesday": "周\n二",
|
||||
"wednesday": "周\n三",
|
||||
"thursday": "周\n四",
|
||||
"friday": "周\n五",
|
||||
"saturday": "周\n六",
|
||||
"sunday": "周\n日"
|
||||
"monday": "周一",
|
||||
"tuesday": "周二",
|
||||
"wednesday": "周三",
|
||||
"thursday": "周四",
|
||||
"friday": "周五",
|
||||
"saturday": "周六",
|
||||
"sunday": "周日"
|
||||
},
|
||||
"processTrafficFailed": "处理流量日志数据失败"
|
||||
},
|
||||
@@ -256,7 +256,7 @@
|
||||
"rewardDetails": "奖励明细 >",
|
||||
"steps": "邀请步骤",
|
||||
"inviteFriend": "邀请好友",
|
||||
"acceptInvite": "好友接受邀请\n下单并注册",
|
||||
"acceptInvite": "好友接受邀请下单并注册",
|
||||
"getReward": "获得奖励",
|
||||
"shareLink": "分享链接",
|
||||
"shareQR": "分享二维码",
|
||||
@@ -415,7 +415,7 @@
|
||||
"content": "是否立即更新?",
|
||||
"updateNow": "立即更新",
|
||||
"updateLater": "稍后再说",
|
||||
"defaultContent": "1. 优化应用性能\n2. 修复已知问题\n3. 提升用户体验"
|
||||
"defaultContent": "1. 优化应用性能2. 修复已知问题3. 提升用户体验"
|
||||
},
|
||||
"country": {
|
||||
"cn": "中国",
|
||||
|
||||
@@ -170,13 +170,13 @@
|
||||
"longestConnection": "最長連接時間",
|
||||
"days": "{days}天",
|
||||
"daysOfWeek": {
|
||||
"monday": "週\n一",
|
||||
"tuesday": "週\n二",
|
||||
"wednesday": "週\n三",
|
||||
"thursday": "週\n四",
|
||||
"friday": "週\n五",
|
||||
"saturday": "週\n六",
|
||||
"sunday": "週\n日"
|
||||
"monday": "週一",
|
||||
"tuesday": "週二",
|
||||
"wednesday": "週三",
|
||||
"thursday": "週四",
|
||||
"friday": "週五",
|
||||
"saturday": "週六",
|
||||
"sunday": "週日"
|
||||
},
|
||||
"processTrafficFailed": "處理流量日誌資料失敗"
|
||||
},
|
||||
@@ -194,7 +194,7 @@
|
||||
"rewardDetails": "獎勵明細 >",
|
||||
"steps": "邀請步驟",
|
||||
"inviteFriend": "邀請好友",
|
||||
"acceptInvite": "好友接受邀請\n下單並註冊",
|
||||
"acceptInvite": "好友接受邀請下單並註冊",
|
||||
"getReward": "獲得獎勵",
|
||||
"shareLink": "分享連結",
|
||||
"shareQR": "分享二維碼",
|
||||
@@ -332,7 +332,7 @@
|
||||
"content": "是否立即更新?",
|
||||
"updateNow": "立即更新",
|
||||
"updateLater": "稍後",
|
||||
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
|
||||
"defaultContent": "1. 優化應用性能2. 修復已知問題3. 改進用戶體驗"
|
||||
},
|
||||
"orderStatus": {
|
||||
"title": "訂單狀態",
|
||||
|
||||
@@ -5,6 +5,8 @@ targets:
|
||||
options:
|
||||
explicit_to_json: true
|
||||
drift_dev:
|
||||
generate_for:
|
||||
- lib/**.drift
|
||||
options:
|
||||
store_date_time_values_as_text: true
|
||||
slang_build_runner:
|
||||
|
||||
+6
-1
@@ -1,4 +1,5 @@
|
||||
name: BearVPN
|
||||
app_name: BearVPN
|
||||
version: 1.0.0
|
||||
build_number: 1
|
||||
targets:
|
||||
@@ -10,4 +11,8 @@ targets:
|
||||
pkg:
|
||||
enable: true
|
||||
# 不签名
|
||||
sign: false
|
||||
sign: false
|
||||
windows:
|
||||
exe:
|
||||
enable: true
|
||||
icon: assets/images/tray_icon.ico
|
||||
+2
-2
@@ -5,6 +5,6 @@
|
||||
// Created by GFWFighter on 7/24/1402 AP.
|
||||
//
|
||||
|
||||
BASE_BUNDLE_IDENTIFIER=app.hi.com
|
||||
BASE_BUNDLE_IDENTIFIER=com.taw.hifastvpn
|
||||
SERVICE_IDENTIFIER=com.hi.app
|
||||
DEVELOPMENT_TEAM=3UR892FAP3
|
||||
DEVELOPMENT_TEAM=NJRRF427XB
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.hi.com</string>
|
||||
<string>group.$(BASE_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
@@ -26,6 +26,8 @@ PODS:
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.2.4)
|
||||
- SAMKeychain (1.5.3)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
@@ -42,6 +44,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||
|
||||
@@ -69,6 +72,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
webview_flutter_wkwebview:
|
||||
@@ -87,6 +92,7 @@ SPEC CHECKSUMS:
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
webview_flutter_wkwebview: a4af96a051138e28e29f60101d094683b9f82188
|
||||
|
||||
|
||||
@@ -758,11 +758,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
EXCLUDED_ARCHS = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -788,7 +786,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = dev_p;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
@@ -816,7 +813,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
EXCLUDED_ARCHS = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -867,7 +864,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
EXCLUDED_ARCHS = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -933,7 +930,7 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
@@ -971,11 +968,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -1004,9 +999,9 @@
|
||||
"-lresolv",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
||||
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = hot;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -1023,7 +1018,7 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
||||
@@ -1042,7 +1037,7 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
||||
@@ -1059,7 +1054,7 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
||||
@@ -1161,7 +1156,7 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
@@ -1201,11 +1196,9 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
||||
@@ -1234,9 +1227,9 @@
|
||||
"-lresolv",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
||||
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = dev;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -1259,7 +1252,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
||||
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -1288,6 +1281,7 @@
|
||||
"-lresolv",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
||||
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
<string>需要相机权限以支持拍照功能</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要麦克风权限以支持语音消息功能</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要相册权限以支持图片上传功能</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>需要相册添加权限以保存图片</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要相册权限以支持图片上传功能</string>
|
||||
<key>SERVICE_IDENTIFIER</key>
|
||||
<string>$(SERVICE_IDENTIFIER)</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.hi.com</string>
|
||||
<string>group.com.taw.hifastvpn</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.hi.com</string>
|
||||
<string>group.com.taw.hifastvpn</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
@@ -10,19 +10,20 @@
|
||||
<string>app-store</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>app.myhiddify.com</key>
|
||||
<string>dist.apple.app.myhiddify.com</string>
|
||||
<key>app.myhiddify.com.SingBoxPacketTunnel</key>
|
||||
<string>dist.apple.app.myhiddify.com.singboxpackettunnel</string>
|
||||
<key>com.taw.hifastvpn</key>
|
||||
<string>HiFastVPN-iOS-Pord</string>
|
||||
|
||||
<key>com.taw.hifastvpn.PacketTunnel</key>
|
||||
<string>HiFastVPN-iOS-Pord-PacketTunnel</string>
|
||||
</dict>
|
||||
<key>signingCertificate</key>
|
||||
<string>E2217AF6F3AD11BA09F9FD95E025D7E637F8B081</string>
|
||||
<string>545725EC39FB3B586B4F63F4BF8D9834FDCFE946</string>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>3UR892FAP3</string>
|
||||
<string>NJRRF427XB</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -1309,6 +1309,9 @@ class AppConfig {
|
||||
/// 网站ID
|
||||
String kr_website_id = "";
|
||||
|
||||
/// 设备限制数量
|
||||
String device_limit = '0';
|
||||
|
||||
/// 是否为白天模式
|
||||
bool kr_is_daytime = true;
|
||||
|
||||
@@ -1447,7 +1450,7 @@ class AppConfig {
|
||||
kr_official_telegram = config.kr_official_telegram;
|
||||
kr_official_telephone = config.kr_official_telephone;
|
||||
kr_invitation_link = config.kr_invitation_link;
|
||||
kr_website_id = config.kr_website_id;
|
||||
// kr_website_id = config.kr_website_id;
|
||||
if (config.kr_domains.isNotEmpty) {
|
||||
KRDomain.kr_handleDomains(config.kr_domains);
|
||||
}
|
||||
|
||||
@@ -28,13 +28,10 @@ class KROutboundItem {
|
||||
|
||||
/// URL
|
||||
String url = "";
|
||||
@override
|
||||
String toString() {
|
||||
return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)';
|
||||
}
|
||||
|
||||
/// 服务器类型
|
||||
|
||||
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
|
||||
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
||||
KROutboundItem(KrNodeListItem nodeListItem) {
|
||||
id = nodeListItem.id.toString();
|
||||
protocol = nodeListItem.protocol;
|
||||
@@ -84,13 +81,13 @@ class KROutboundItem {
|
||||
case "vless":
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
|
||||
|
||||
// 智能设置 server_name
|
||||
String serverName = securityConfig["sni"] ?? "";
|
||||
if (serverName.isEmpty) {
|
||||
serverName = nodeListItem.serverAddr;
|
||||
}
|
||||
|
||||
|
||||
config = {
|
||||
"type": "vless",
|
||||
"tag": nodeListItem.name,
|
||||
@@ -115,13 +112,13 @@ class KROutboundItem {
|
||||
case "vmess":
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
|
||||
|
||||
// 智能设置 server_name
|
||||
String serverName = securityConfig["sni"] ?? "";
|
||||
if (serverName.isEmpty) {
|
||||
serverName = nodeListItem.serverAddr;
|
||||
}
|
||||
|
||||
|
||||
config = {
|
||||
"type": "vmess",
|
||||
"tag": nodeListItem.name,
|
||||
@@ -152,7 +149,7 @@ class KROutboundItem {
|
||||
break;
|
||||
case "hysteria":
|
||||
case "hysteria2":
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
config = {
|
||||
@@ -177,14 +174,14 @@ class KROutboundItem {
|
||||
case "trojan":
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
|
||||
|
||||
// 智能设置 server_name
|
||||
String serverName = securityConfig["sni"] ?? "";
|
||||
if (serverName.isEmpty) {
|
||||
// 如果没有配置 SNI,使用服务器地址
|
||||
serverName = nodeListItem.serverAddr;
|
||||
}
|
||||
|
||||
|
||||
config = {
|
||||
"type": "trojan",
|
||||
"tag": nodeListItem.name,
|
||||
@@ -274,6 +271,7 @@ class KROutboundItem {
|
||||
// 🔧 尝试从 config 字段解析 transport 配置
|
||||
Map<String, dynamic>? transportConfig;
|
||||
Map<String, dynamic>? securityConfig;
|
||||
int actualPort = nodeListItem.port; // 🔧 使用局部变量存储实际端口
|
||||
|
||||
// 🔧 关键修复:优先从 protocols 字段解析配置
|
||||
if (nodeListItem.protocols.isNotEmpty) {
|
||||
@@ -307,6 +305,28 @@ class KROutboundItem {
|
||||
}
|
||||
|
||||
if (matchedProtocol != null) {
|
||||
// 🔧 关键修复:只在顶层端口为0时,才使用 protocols 中的端口
|
||||
// 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖
|
||||
if (actualPort == 0 && matchedProtocol['port'] != null) {
|
||||
// 安全解析端口号
|
||||
int protocolPort = 0;
|
||||
final portValue = matchedProtocol['port'];
|
||||
if (portValue is int) {
|
||||
protocolPort = portValue;
|
||||
} else if (portValue is String) {
|
||||
protocolPort = int.tryParse(portValue) ?? 0;
|
||||
}
|
||||
|
||||
if (protocolPort > 0) {
|
||||
actualPort = protocolPort;
|
||||
if (kDebugMode) {
|
||||
print(' ✅ 从 protocols 使用端口: $protocolPort (顶层端口为0)');
|
||||
}
|
||||
}
|
||||
} else if (kDebugMode && matchedProtocol['port'] != null) {
|
||||
print(' ✅ 保留顶层端口: $actualPort (protocols中的端口: ${matchedProtocol['port']})');
|
||||
}
|
||||
|
||||
// 提取 transport 配置
|
||||
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
||||
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
||||
@@ -402,7 +422,7 @@ class KROutboundItem {
|
||||
|
||||
switch (nodeListItem.protocol) {
|
||||
case "shadowsocks":
|
||||
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
||||
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
||||
String finalMethod = nodeListItem.method.isNotEmpty
|
||||
? nodeListItem.method
|
||||
: "2022-blake3-aes-256-gcm";
|
||||
@@ -411,22 +431,21 @@ class KROutboundItem {
|
||||
"type": "shadowsocks",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"method": finalMethod,
|
||||
"password": nodeListItem.uuid
|
||||
};
|
||||
if (kDebugMode) {
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 使用加密方法: $finalMethod');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 完整配置: $config');
|
||||
print('📄 完整配置 JSON:');
|
||||
print(jsonEncode(config));
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
}
|
||||
break;
|
||||
case "vless":
|
||||
// 判断是否为域名(非IP地址)
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
@@ -436,17 +455,25 @@ class KROutboundItem {
|
||||
serverName = securityConfig['sni'].toString();
|
||||
}
|
||||
|
||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
||||
final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
||||
// 🔧 关键修复:智能判断是否启用 TLS
|
||||
// 1. 优先使用 securityConfig['tls_enabled']
|
||||
// 2. 如果没有明确配置,根据端口和域名智能判断
|
||||
bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
|
||||
|
||||
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
|
||||
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
|
||||
vlessTlsEnabled = false;
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled');
|
||||
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||
}
|
||||
|
||||
config = {
|
||||
"type": "vless",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"uuid": nodeListItem.uuid,
|
||||
if (transportConfig != null) "transport": transportConfig,
|
||||
if (vlessTlsEnabled) "tls": {
|
||||
@@ -460,14 +487,27 @@ class KROutboundItem {
|
||||
}
|
||||
};
|
||||
if (kDebugMode) {
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 完整配置: $config');
|
||||
print('📋 原始节点信息:');
|
||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||
print(' - port: ${nodeListItem.port}');
|
||||
print(' - uuid: ${nodeListItem.uuid}');
|
||||
print(' - protocols: ${nodeListItem.protocols}');
|
||||
print('🔐 安全配置:');
|
||||
print(' - TLS 启用: $vlessTlsEnabled');
|
||||
print(' - 是域名: $isDomain');
|
||||
print(' - server_name: $serverName');
|
||||
print(' - securityConfig: $securityConfig');
|
||||
print('📡 传输配置:');
|
||||
print(' - transportConfig: $transportConfig');
|
||||
print('📄 最终生成的完整配置 JSON:');
|
||||
print(jsonEncode(config));
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
}
|
||||
break;
|
||||
case "vmess":
|
||||
// 判断是否为域名(非IP地址)
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
@@ -477,17 +517,25 @@ class KROutboundItem {
|
||||
serverName = securityConfig['sni'].toString();
|
||||
}
|
||||
|
||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
||||
final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
||||
// 🔧 关键修复:智能判断是否启用 TLS
|
||||
// 1. 优先使用 securityConfig['tls_enabled']
|
||||
// 2. 如果没有明确配置,根据端口和域名智能判断
|
||||
bool tlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
|
||||
|
||||
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
|
||||
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
|
||||
tlsEnabled = false;
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('🔐 TLS 状态: enabled=$tlsEnabled');
|
||||
print('🔐 VMess TLS 状态: enabled=$tlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||
}
|
||||
|
||||
config = {
|
||||
"type": "vmess",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"uuid": nodeListItem.uuid,
|
||||
"alter_id": 0,
|
||||
"security": "auto",
|
||||
@@ -503,14 +551,27 @@ class KROutboundItem {
|
||||
}
|
||||
};
|
||||
if (kDebugMode) {
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 完整配置: $config');
|
||||
print('📋 原始节点信息:');
|
||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||
print(' - port: ${nodeListItem.port}');
|
||||
print(' - uuid: ${nodeListItem.uuid}');
|
||||
print(' - protocols: ${nodeListItem.protocols}');
|
||||
print('🔐 安全配置:');
|
||||
print(' - TLS 启用: $tlsEnabled');
|
||||
print(' - 是域名: $isDomain');
|
||||
print(' - server_name: $serverName');
|
||||
print(' - securityConfig: $securityConfig');
|
||||
print('📡 传输配置:');
|
||||
print(' - transportConfig: $transportConfig');
|
||||
print('📄 最终生成的完整配置 JSON:');
|
||||
print(jsonEncode(config));
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
}
|
||||
break;
|
||||
case "trojan":
|
||||
// 判断是否为域名(非IP地址)
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
@@ -524,7 +585,7 @@ class KROutboundItem {
|
||||
"type": "trojan",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"password": nodeListItem.uuid,
|
||||
if (transportConfig != null) "transport": transportConfig,
|
||||
"tls": {
|
||||
@@ -546,7 +607,7 @@ class KROutboundItem {
|
||||
break;
|
||||
case "hysteria":
|
||||
case "hysteria2":
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
if (kDebugMode) {
|
||||
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
|
||||
/// 表示出站项列表的模型类
|
||||
class KrOutboundsList {
|
||||
|
||||
|
||||
|
||||
|
||||
/// 服务器分组
|
||||
final List<KRGroupOutboundList> groupOutboundList = []; // 存储服务器分组的列表
|
||||
@@ -18,20 +18,20 @@ class KrOutboundsList {
|
||||
|
||||
/// 全部列表
|
||||
final List<KROutboundItem> allList = []; // 存储国家分组的列表
|
||||
|
||||
|
||||
// 配置json
|
||||
final List<Map<String,dynamic>> configJsonList = [];
|
||||
final List<Map<String,dynamic>> configJsonList = [];
|
||||
|
||||
/// 标签列表
|
||||
final Map<String,KROutboundItem> keyList = {}; // 存储国家分组的列表
|
||||
|
||||
|
||||
|
||||
/// 处理出站项并将其分组
|
||||
/// [list] 是要处理的出站项列表
|
||||
void processOutboundItems(List<KrNodeListItem> list,List<KRNodeGroupListItem> groupList) {
|
||||
final Map<String, List<KROutboundItem>> tagGroups = {};
|
||||
final Map<String, List<KROutboundItem>> countryGroups = {};
|
||||
|
||||
|
||||
// 用于追踪已使用的标签
|
||||
final Map<String, int> tagCounter = {};
|
||||
|
||||
@@ -90,9 +90,9 @@ class KrOutboundsList {
|
||||
|
||||
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
|
||||
for (var tag in tagGroups.keys) {
|
||||
final item = KRGroupOutboundList(
|
||||
final item = KRGroupOutboundList(
|
||||
tag: tag, outboundList: tagGroups[tag]!);
|
||||
|
||||
|
||||
for (var group in groupList) {
|
||||
if (item.tag == group.name) {
|
||||
item.icon = group.icon;
|
||||
|
||||
@@ -7,16 +7,16 @@ class KRIsRegister {
|
||||
KRIsRegister({this.kr_isRegister = false});
|
||||
|
||||
KRIsRegister.fromJson(Map<String, dynamic> json) {
|
||||
kr_isRegister = json['Status'] == "true" || json['Status'] == true
|
||||
kr_isRegister = json['exist'] == "true" || json['exist'] == true
|
||||
? true
|
||||
: false || json['status'] == "true" || json['status'] == true
|
||||
: false || json['exist'] == "true" || json['exist'] == true
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['Status'] = kr_isRegister;
|
||||
data['exist'] = kr_isRegister;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
class KRNodeList {
|
||||
final List<KrNodeListItem> list;
|
||||
final List<KrNodeListItem> list;
|
||||
final String subscribeId;
|
||||
final String startTime;
|
||||
final String expireTime;
|
||||
@@ -35,8 +35,8 @@ class KRNodeList {
|
||||
// 查找 id 匹配的订阅项
|
||||
try {
|
||||
subscribeData = listData.firstWhere(
|
||||
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
||||
orElse: () => listData[0] as Map<String, dynamic>
|
||||
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
||||
orElse: () => listData[0] as Map<String, dynamic>
|
||||
) as Map<String, dynamic>;
|
||||
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
|
||||
} catch (e) {
|
||||
@@ -70,7 +70,7 @@ class KRNodeList {
|
||||
|
||||
class KrNodeListItem {
|
||||
final int id;
|
||||
String name;
|
||||
String name;
|
||||
final String uuid;
|
||||
final String protocol;
|
||||
final String relayMode;
|
||||
@@ -136,25 +136,68 @@ class KrNodeListItem {
|
||||
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
||||
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
|
||||
|
||||
// 🔧 如果有 protocols 字段,从中解析 port 和 cipher
|
||||
// 🔧 打印原始节点 JSON(用于调试)
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('📥 收到节点 API 原始数据:', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('节点名称: ${json['name']}', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('协议类型: ${json['protocol']}', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('完整 JSON:', tag: 'NodeList');
|
||||
KRLogUtil.kr_i(jsonEncode(json), tag: 'NodeList');
|
||||
|
||||
// 🔧 如果有 protocols 字段,从中解析 cipher(但不覆盖顶层的 port)
|
||||
if (protocols.isNotEmpty) {
|
||||
try {
|
||||
final protocolsList = jsonDecode(protocols) as List;
|
||||
final currentProtocol = json['protocol']?.toString().toLowerCase() ?? '';
|
||||
|
||||
KRLogUtil.kr_i('📋 protocols 字段内容 (${protocolsList.length} 个协议):', tag: 'NodeList');
|
||||
for (var i = 0; i < protocolsList.length; i++) {
|
||||
KRLogUtil.kr_i(' 协议 $i: ${jsonEncode(protocolsList[i])}', tag: 'NodeList');
|
||||
}
|
||||
|
||||
if (protocolsList.isNotEmpty) {
|
||||
final firstProtocol = protocolsList[0] as Map<String, dynamic>;
|
||||
// 优先使用 protocols 中的配置
|
||||
if (firstProtocol['port'] != null) {
|
||||
port = _parseIntSafely(firstProtocol['port']);
|
||||
// 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个
|
||||
Map<String, dynamic>? matchedProtocolConfig;
|
||||
|
||||
// 尝试找到协议类型匹配的配置
|
||||
for (var protocolConfig in protocolsList) {
|
||||
final configMap = protocolConfig as Map<String, dynamic>;
|
||||
// 🔧 修复:API 使用的字段名是 'type',不是 'protocol'
|
||||
final protocolType = (configMap['type'] ?? configMap['protocol'])?.toString().toLowerCase() ?? '';
|
||||
|
||||
// 检查是否匹配(支持 shadowsocks/ss, vmess, vless, trojan 等)
|
||||
if (protocolType == currentProtocol ||
|
||||
(currentProtocol == 'shadowsocks' && protocolType == 'ss') ||
|
||||
(currentProtocol == 'ss' && protocolType == 'shadowsocks')) {
|
||||
matchedProtocolConfig = configMap;
|
||||
KRLogUtil.kr_i('🎯 找到匹配的协议配置: $protocolType', tag: 'NodeList');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstProtocol['cipher'] != null && firstProtocol['cipher'].toString().isNotEmpty) {
|
||||
method = firstProtocol['cipher'].toString();
|
||||
|
||||
// 如果没找到匹配的,使用第一个配置(兼容旧API)
|
||||
final targetProtocol = matchedProtocolConfig ?? (protocolsList[0] as Map<String, dynamic>);
|
||||
|
||||
// 🔧 关键修复:只在顶层没有 port 字段时,才使用 protocols 中的端口
|
||||
// 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖
|
||||
if (port == 0 && targetProtocol['port'] != null) {
|
||||
port = _parseIntSafely(targetProtocol['port']);
|
||||
KRLogUtil.kr_i('✅ 从 protocols 解析端口: $port', tag: 'NodeList');
|
||||
} else {
|
||||
KRLogUtil.kr_i('✅ 保留顶层端口: $port (protocols中的端口: ${targetProtocol['port']})', tag: 'NodeList');
|
||||
}
|
||||
|
||||
// 提取 cipher(加密方法)
|
||||
if (targetProtocol['cipher'] != null && targetProtocol['cipher'].toString().isNotEmpty) {
|
||||
method = targetProtocol['cipher'].toString();
|
||||
KRLogUtil.kr_i('✅ 从 protocols 解析 cipher: $method', tag: 'NodeList');
|
||||
}
|
||||
KRLogUtil.kr_i('从 protocols 解析: port=$port, cipher=$method', tag: 'NodeList');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('解析 protocols 字段失败: $e', tag: 'NodeList');
|
||||
KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList');
|
||||
}
|
||||
}
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
|
||||
|
||||
return KrNodeListItem(
|
||||
id: _parseIntSafely(json['id']),
|
||||
|
||||
@@ -63,6 +63,7 @@ class KRSiteInfo {
|
||||
final String customHtml;
|
||||
final String customData;
|
||||
final String crispId;
|
||||
final String deviceLimit;
|
||||
|
||||
KRSiteInfo({
|
||||
required this.host,
|
||||
@@ -73,10 +74,12 @@ class KRSiteInfo {
|
||||
required this.customHtml,
|
||||
required this.customData,
|
||||
required this.crispId,
|
||||
required this.deviceLimit,
|
||||
});
|
||||
|
||||
factory KRSiteInfo.fromJson(Map<String, dynamic> json) {
|
||||
String crispId = '0';
|
||||
String deviceLimit = '0';
|
||||
|
||||
// 尝试解析 custom_data 中的 kr_website_id
|
||||
try {
|
||||
@@ -89,6 +92,16 @@ class KRSiteInfo {
|
||||
// 解析失败时使用默认值
|
||||
}
|
||||
|
||||
// 尝试解析 custom_data 中的 deviceLimit
|
||||
try {
|
||||
final customDataStr = json['custom_data'] ?? '';
|
||||
if (customDataStr.isNotEmpty) {
|
||||
final customDataJson = jsonDecode(customDataStr) as Map<String, dynamic>;
|
||||
deviceLimit = (customDataJson['deviceLimit'] ?? 0).toString();
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败时使用默认值
|
||||
}
|
||||
return KRSiteInfo(
|
||||
host: json['host'] ?? '',
|
||||
siteName: json['site_name'] ?? '',
|
||||
@@ -98,6 +111,7 @@ class KRSiteInfo {
|
||||
customHtml: json['custom_html'] ?? '',
|
||||
customData: json['custom_data'] ?? '',
|
||||
crispId: crispId,
|
||||
deviceLimit: deviceLimit,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,76 +13,86 @@ import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/hi_menu_list_item.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart';
|
||||
|
||||
class HIHelpView extends GetView<HIHelpController> {
|
||||
class HIHelpView extends GetView<HIHelpController> implements HasSwipeConfig {
|
||||
const HIHelpView({super.key});
|
||||
|
||||
@override
|
||||
SwipeConfig get swipeConfig =>
|
||||
SwipeConfig(enableLeft: true, onLeft: () => Get.back());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
return HIBaseScaffold(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => EasyRefresh(
|
||||
// controller: controller.refreshController,
|
||||
// onRefresh: controller.kr_onRefresh,
|
||||
// onLoad: controller.kr_onLoadMore,
|
||||
// header: const DeliveryHeader(
|
||||
// triggerOffset: 50.0,
|
||||
// springRebound: true,
|
||||
// ),
|
||||
// footer: const DeliveryFooter(
|
||||
// triggerOffset: 50.0,
|
||||
// springRebound: true,
|
||||
// ),
|
||||
onRefresh: null, // 设置为 null 来禁用下拉刷新
|
||||
onLoad: null, // 设置为 null 来禁用上拉加载
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 0.w), // 滚动条与内容间距
|
||||
child: HiFixedScrollbar(
|
||||
controller: scrollController,
|
||||
child: ListView.builder(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => EasyRefresh(
|
||||
// controller: controller.refreshController,
|
||||
// onRefresh: controller.kr_onRefresh,
|
||||
// onLoad: controller.kr_onLoadMore,
|
||||
// header: const DeliveryHeader(
|
||||
// triggerOffset: 50.0,
|
||||
// springRebound: true,
|
||||
// ),
|
||||
// footer: const DeliveryFooter(
|
||||
// triggerOffset: 50.0,
|
||||
// springRebound: true,
|
||||
// ),
|
||||
onRefresh: null, // 设置为 null 来禁用下拉刷新
|
||||
onLoad: null, // 设置为 null 来禁用上拉加载
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 0.w), // 滚动条与内容间距
|
||||
child: HiFixedScrollbar(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
itemCount: controller.kr_messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = controller.kr_messages[index];
|
||||
final collapsibleItemData = HICollapsibleItem(
|
||||
title: message.title,
|
||||
content: message.content,
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.w),
|
||||
child: HICollapsibleItemWidget(item: collapsibleItemData),
|
||||
);
|
||||
},
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.only(left: 40.w - 20, right: 40.w),
|
||||
itemCount: controller.kr_messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = controller.kr_messages[index];
|
||||
final collapsibleItemData = HICollapsibleItem(
|
||||
title: message.title,
|
||||
content: message.content,
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.w),
|
||||
child: HICollapsibleItemWidget(
|
||||
item: collapsibleItemData),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 60.w),
|
||||
// 增加在线客服入口
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: MenuListItem(
|
||||
// 2. 创建一个 MenuItem 数据对象
|
||||
item: MenuItem(
|
||||
iconName: 'icon-5',
|
||||
title: '在线客服',
|
||||
// 3. 使用 onTap 回调来处理跳转逻辑
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.KR_CRISP);
|
||||
},
|
||||
SizedBox(height: 60.w),
|
||||
// 增加在线客服入口
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: MenuListItem(
|
||||
// 2. 创建一个 MenuItem 数据对象
|
||||
item: MenuItem(
|
||||
iconName: 'icon-5',
|
||||
title: '在线客服',
|
||||
// 3. 使用 onTap 回调来处理跳转逻辑
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.KR_CRISP);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30.w)
|
||||
],
|
||||
SizedBox(height: 30.w)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ class HIMenuController extends GetxController {
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
// 版本号
|
||||
final RxString kr_version = ''.obs;
|
||||
final RxString kr_buildNumber = ''.obs;
|
||||
final RxBool kr_showBuild = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -23,5 +25,10 @@ class HIMenuController extends GetxController {
|
||||
Future<void> _kr_getVersion() async {
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
kr_version.value = packageInfo.version;
|
||||
kr_buildNumber.value = packageInfo.buildNumber;
|
||||
}
|
||||
|
||||
void toggleVersionDisplay() {
|
||||
kr_showBuild.value = !kr_showBuild.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart';
|
||||
import '../controllers/hi_menu_controller.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||
@@ -16,9 +18,15 @@ import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/hi_menu_list_item.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/user_info_card.dart';
|
||||
|
||||
class HIMenuView extends GetView<HIMenuController> {
|
||||
class HIMenuView extends GetView<HIMenuController> implements HasSwipeConfig {
|
||||
const HIMenuView({super.key});
|
||||
|
||||
@override
|
||||
SwipeConfig get swipeConfig => SwipeConfig(
|
||||
enableRight: true,
|
||||
onRight: () => Get.offNamed(Routes.KR_HOME),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HIBaseScaffold(
|
||||
@@ -37,19 +45,26 @@ class HIMenuView extends GetView<HIMenuController> {
|
||||
mainAxisSize: MainAxisSize.min, // 让 Column 包裹内容
|
||||
children: [
|
||||
Obx(() {
|
||||
final account = KRAppRunData.getInstance().kr_account.value ?? '未登录';
|
||||
final account =
|
||||
KRAppRunData.getInstance().kr_account.value;
|
||||
final isDeviceLogin =
|
||||
account != null && account.startsWith('9000');
|
||||
final accountText = (account == null || isDeviceLogin)
|
||||
? '待绑定'
|
||||
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||
return UserInfoCard(
|
||||
controller: controller,
|
||||
userId: account,
|
||||
userId: accountText,
|
||||
);
|
||||
}),
|
||||
|
||||
SizedBox(height: 10.h), // 卡片和菜单列表的间距
|
||||
SizedBox(height: 10), // 卡片和菜单列表的间距
|
||||
|
||||
// ListView.separated 现在也会继承 Padding 的约束
|
||||
ListView.separated(
|
||||
shrinkWrap: true, // 让 ListView 高度自适应内容
|
||||
physics: const NeverScrollableScrollPhysics(), // 在 Stack 中,禁用其自身的滚动
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(), // 在 Stack 中,禁用其自身的滚动
|
||||
itemCount: _menuItems.length,
|
||||
// 渲染每一项
|
||||
itemBuilder: (context, index) {
|
||||
@@ -66,19 +81,20 @@ class HIMenuView extends GetView<HIMenuController> {
|
||||
],
|
||||
),
|
||||
// 版本号信息
|
||||
Obx((){
|
||||
// 1. 从 Obx 的回调函数中 return 一个 Widget
|
||||
Obx(() {
|
||||
return Positioned(
|
||||
bottom: 40.0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Text(
|
||||
'当前版本:${controller.kr_version.value}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
// 2. 建议使用更稳定的颜色,避免主题切换时出现问题
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 10.w,
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () => controller.toggleVersionDisplay(),
|
||||
child: Text(
|
||||
'当前版本:${controller.kr_showBuild.value ? '${controller.kr_version.value}.${controller.kr_buildNumber.value}' : controller.kr_version.value}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 10.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -89,7 +105,6 @@ class HIMenuView extends GetView<HIMenuController> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const List<MenuItem> _menuItems = [
|
||||
MenuItem(
|
||||
iconName: 'icon-1',
|
||||
@@ -116,4 +131,4 @@ const List<MenuItem> _menuItems = [
|
||||
title: '在线客服',
|
||||
route: Routes.KR_CRISP,
|
||||
),
|
||||
];
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/controllers/hi_menu_controller.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart';
|
||||
|
||||
/// 用户信息展示卡片 Widget
|
||||
///
|
||||
@@ -30,10 +31,10 @@ class UserInfoCard extends StatelessWidget {
|
||||
// 让整个卡片的透明区域也能响应点击
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(7.w, 4, 18.w, 4),
|
||||
padding: EdgeInsets.fromLTRB(5, 4, 18, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(35.5.r),
|
||||
borderRadius: BorderRadius.circular(35.5),
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2.0, // 👇 核心改动: 将边框宽度设置为 2.0
|
||||
@@ -42,74 +43,35 @@ class UserInfoCard extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 18.r,
|
||||
radius: 18,
|
||||
backgroundColor: Colors.white,
|
||||
child: KrLocalImage(
|
||||
imageName: 'hi-home-logo',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'ID: $userId',
|
||||
userId,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
String expiryText;
|
||||
|
||||
if (currentSubscribe == null) {
|
||||
expiryText = '尚未购买套餐';
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(now)) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
} else {
|
||||
final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
// 2. 拼接成最终的字符串
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiryText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
KRSubscriptionExpiryText(
|
||||
expireTimeProvider: () => controller.kr_subscribeService
|
||||
.kr_currentSubscribe.value?.expireTime,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -124,4 +86,4 @@ class UserInfoCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
|
||||
import '../../../localization/app_translations.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
||||
|
||||
class HINodeListController extends GetxController {
|
||||
class HINodeListController extends GetxController with WidgetsBindingObserver {
|
||||
/// 订阅服务
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
|
||||
/// 首页服务
|
||||
final KRHomeController homeController = Get.find<KRHomeController>();
|
||||
|
||||
/// 调试模式状态
|
||||
final RxBool isDebugMode = false.obs;
|
||||
|
||||
/// 模式按钮点击计数器
|
||||
int modeButtonClickCount = 0;
|
||||
|
||||
/// 最后一次点击时间
|
||||
DateTime? lastModeButtonClickTime;
|
||||
|
||||
/// 获取连接类型字符串
|
||||
String kr_getConnectionTypeString() {
|
||||
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
||||
@@ -30,15 +41,87 @@ class HINodeListController extends GetxController {
|
||||
KRLogUtil.kr_i('连接类型已更新为: $type', tag: 'HINodeListController');
|
||||
// 这里可以添加其他需要的逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当前有选择的国家,触发重选检查
|
||||
if (homeController.currentSelectedCountry.isNotEmpty) {
|
||||
KRLogUtil.kr_i('连接类型更新后,检查是否需要重选节点', tag: 'HINodeListController');
|
||||
// 延迟一下让连接类型更新完成
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
// homeController.checkCountryReselection(KRSingBoxImp.instance.kr_activeGroups);
|
||||
});
|
||||
/// 处理模式按钮点击(用于激活调试模式)
|
||||
void kr_handleModeButtonClick() {
|
||||
final now = DateTime.now();
|
||||
|
||||
// 检查是否在2秒内连续点击
|
||||
if (lastModeButtonClickTime != null &&
|
||||
now.difference(lastModeButtonClickTime!).inSeconds > 2) {
|
||||
// 超过2秒,重置计数器
|
||||
modeButtonClickCount = 0;
|
||||
}
|
||||
|
||||
modeButtonClickCount++;
|
||||
lastModeButtonClickTime = now;
|
||||
|
||||
KRLogUtil.kr_i('模式按钮点击次数: $modeButtonClickCount',
|
||||
tag: 'HINodeListController');
|
||||
|
||||
if (modeButtonClickCount >= 5) {
|
||||
// 激活调试模式
|
||||
isDebugMode.value = true;
|
||||
modeButtonClickCount = 0; // 重置计数器
|
||||
KRLogUtil.kr_i('🐛 调试模式已激活!', tag: 'HINodeListController');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取要显示的节点列表(根据调试模式过滤)
|
||||
|
||||
/// 重置调试模式
|
||||
void kr_resetDebugMode() {
|
||||
isDebugMode.value = false;
|
||||
modeButtonClickCount = 0;
|
||||
lastModeButtonClickTime = null;
|
||||
KRLogUtil.kr_i('调试模式已重置', tag: 'HINodeListController');
|
||||
}
|
||||
|
||||
Future<void> kr_handleRefresh() async {
|
||||
await kr_subscribeService.kr_refreshAll();
|
||||
try {
|
||||
final savedNode =
|
||||
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
|
||||
if (savedNode != null && savedNode.isNotEmpty) {
|
||||
homeController.kr_currentNodeName.value = savedNode;
|
||||
homeController.kr_cutTag.value = savedNode;
|
||||
homeController.kr_cutSeletedTag.value = savedNode;
|
||||
}
|
||||
} catch (_) {}
|
||||
if (!homeController.kr_isLatency.value) {
|
||||
homeController.kr_urlTest();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
if (homeController.kr_isLatency.value) return;
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
|
||||
homeController.kr_urlTest();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
// 当应用从后台切换回前台时
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (homeController.kr_isLatency.value) return;
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
|
||||
homeController.kr_urlTest();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// hi_node_list_view.dart
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@@ -9,6 +8,7 @@ import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
|
||||
import '../../../model/business/kr_outbound_item.dart';
|
||||
import '../../../utils/kr_log_util.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
|
||||
import '../controllers/hi_node_list_controller.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/models/kr_home_views_status.dart';
|
||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
@@ -24,41 +24,18 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
static const Color krModernGreen = Color(0xFF4CAF50);
|
||||
static const Color krModernGreenLight = Color(0xFF81C784);
|
||||
|
||||
// 存储随机延迟值,用于UI展示
|
||||
static final Map<String, int> _fakeDelays = {};
|
||||
|
||||
/// 获取用于显示的延迟值
|
||||
int _getDisplayDelay(HINodeListController controller, KROutboundItem item) {
|
||||
return item.urlTestDelay.value;
|
||||
// if (controller.homeController.kr_isConnected.value) {
|
||||
//
|
||||
// }
|
||||
// if (!_fakeDelays.containsKey(item.tag)) {
|
||||
// final random = Random();
|
||||
// _fakeDelays[item.tag] = 30 + random.nextInt(71); // 30-100ms
|
||||
// }
|
||||
// return _fakeDelays[item.tag] ?? 0;
|
||||
}
|
||||
|
||||
/// 获取分组内最快节点的延迟值(单位:ms)
|
||||
/// 如果列表为空,返回 0
|
||||
int getFastestNodeDelay(
|
||||
HINodeListController controller,
|
||||
List<KROutboundItem> outboundList,
|
||||
) {
|
||||
HINodeListController controller,
|
||||
List<KROutboundItem> outboundList,
|
||||
) {
|
||||
if (outboundList.isEmpty) return 0;
|
||||
|
||||
// 过滤掉不可用节点(延迟标记为 65535 或负数)
|
||||
final validNodes = outboundList.where((node) {
|
||||
final delay = node.urlTestDelay.value;
|
||||
return delay > 0 && delay < 65535;
|
||||
}).toList();
|
||||
|
||||
if (validNodes.isEmpty) return 0;
|
||||
|
||||
// 返回最小延迟值
|
||||
validNodes.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
|
||||
return validNodes.first.urlTestDelay.value;
|
||||
outboundList
|
||||
.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
|
||||
return outboundList.first.urlTestDelay.value;
|
||||
}
|
||||
|
||||
/// 找出延迟最低(最快)的节点对象
|
||||
@@ -86,23 +63,20 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
Widget build(BuildContext context) {
|
||||
// 1. 使用 Material 作为根组件,确保 InkWell 的水波纹效果正常
|
||||
// 并设置透明背景,让父组件的背景可以透出来
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildSubscribeList(context)
|
||||
// child: _kr_buildRegionList(context)
|
||||
);
|
||||
return Obx(() {
|
||||
// Material 保持透明背景,让 InkWell 水波纹正常
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: controller.isDebugMode.value
|
||||
? _buildSubscribeList(context)
|
||||
: _kr_buildRegionList(context),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 构建国家/地区分组列表
|
||||
Widget _kr_buildRegionList(BuildContext context) {
|
||||
return Obx(() {
|
||||
// 自动触发延迟测试(仅在未连接状态下)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!controller.homeController.kr_isConnected.value && !controller.homeController.kr_isLatency.value) {
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
|
||||
controller.homeController.kr_urlTest();
|
||||
}
|
||||
});
|
||||
return _kr_buildListContainer(
|
||||
context,
|
||||
child: ListView(
|
||||
@@ -110,21 +84,23 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||
children: [
|
||||
if (controller.kr_subscribeService.countryOutboundList.isEmpty)
|
||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noRegions)
|
||||
_buildEmptyListPlaceholder(
|
||||
context, AppTranslations.kr_home.noRegions)
|
||||
else ...[
|
||||
InkWell(
|
||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||
onTap: () async {
|
||||
try {
|
||||
final success =
|
||||
await controller.homeController.kr_performNodeSwitch('auto');
|
||||
controller.homeController
|
||||
.kr_setSelectedCountryTag('auto');
|
||||
final success = await controller.homeController
|
||||
.kr_performNodeSwitch('auto');
|
||||
if (success) {
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('Auto选项切换异常: $e',
|
||||
tag: 'NodeListView');
|
||||
KRLogUtil.kr_e('Auto选项切换异常: $e', tag: 'NodeListView');
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@@ -136,7 +112,8 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -180,143 +157,193 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_coutryText.value == 'auto'
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
Obx(() =>
|
||||
controller.homeController.kr_selectedCountryTag.value ==
|
||||
'auto'
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
||||
...controller.kr_subscribeService.countryOutboundList
|
||||
.map((country) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// 自动选择这个国家下的节点延迟中最快的
|
||||
controller.homeController.onCountrySelected(country.country);
|
||||
onTap: () async {
|
||||
try {
|
||||
controller.homeController
|
||||
.kr_setSelectedCountryTag(country.country);
|
||||
final fastest = findFastestNode(country.outboundList);
|
||||
final success = await controller.homeController
|
||||
.kr_performNodeSwitch(fastest.tag);
|
||||
if (success) {
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('国家选择切换异常: $e', tag: 'NodeListView');
|
||||
}
|
||||
},
|
||||
child: _kr_buildCountryListItem(context, country: country),
|
||||
);
|
||||
}).toList(),
|
||||
]
|
||||
] //
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 构建默认的订阅节点列表
|
||||
Widget _buildSubscribeList(BuildContext context) {
|
||||
return Obx(() {
|
||||
|
||||
// 自动触发延迟测试(仅在未连接状态下)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!controller.homeController.kr_isConnected.value && !controller.homeController.kr_isLatency.value) {
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
|
||||
controller.homeController.kr_urlTest();
|
||||
}
|
||||
});
|
||||
return _kr_buildListContainer(
|
||||
context,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||
children: [
|
||||
if (controller.kr_subscribeService.allList.isEmpty)
|
||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
||||
else ...[
|
||||
InkWell(
|
||||
onTap: () {
|
||||
controller.homeController.kr_selectNode('auto');
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32.w,
|
||||
height: 22.w,
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||
children: [
|
||||
...[
|
||||
InkWell(
|
||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||
onTap: () async {
|
||||
try {
|
||||
final success = await controller.homeController
|
||||
.kr_performNodeSwitch('auto');
|
||||
if (success) {
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('Auto选项切换异常: $e', tag: 'NodeListView');
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
// 2. 设置背景色为主题色
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
// 4. 使用 Center 来确保内部的图片水平和垂直居中
|
||||
child: Center(
|
||||
child: KrLocalImage(
|
||||
imageName: "hi-home-logo",
|
||||
width: 14.w,
|
||||
height: 14.h,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'自动匹配最快网络', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600, // 600 加粗
|
||||
Container(
|
||||
width: 32.w,
|
||||
height: 22.w,
|
||||
decoration: BoxDecoration(
|
||||
// 2. 设置背景色为主题色
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
// 4. 使用 Center 来确保内部的图片水平和垂直居中
|
||||
child: Center(
|
||||
child: KrLocalImage(
|
||||
imageName: "hi-home-logo",
|
||||
width: 14.w,
|
||||
height: 14.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||
Text(
|
||||
'根据网络IP自动匹配最快线路', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'自动匹配最快网络', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600, // 600 加粗
|
||||
),
|
||||
),
|
||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||
Text(
|
||||
'根据网络IP自动匹配最快线路', // 默认文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
/* Obx(() {
|
||||
// 当选择全局 auto 时,显示当前选中的节点信息
|
||||
if (controller.homeController.kr_cutTag.value == 'auto') {
|
||||
final autoNodeInfo = controller.homeController.kr_getGlobalAutoSelectedNode();
|
||||
if (autoNodeInfo != null) {
|
||||
return Text(
|
||||
'当前: ${autoNodeInfo['tag']} (${autoNodeInfo['delay']}ms)',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
'根据网络IP自动匹配最快线路', // 默认文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}),*/
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() =>
|
||||
controller.homeController.kr_cutTag.value == 'auto'
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_cutTag.value == 'auto'
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
...controller.kr_subscribeService.allList().map((item) {
|
||||
return InkWell(
|
||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||
onTap: () async {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 用户点击节点: ${item.tag}');
|
||||
final success = await controller.homeController
|
||||
.kr_performNodeSwitch(item.tag);
|
||||
if (success) {
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('节点切换异常: $e', tag: 'NodeListView');
|
||||
}
|
||||
},
|
||||
child: _kr_buildNodeListItem(context, item: item),
|
||||
);
|
||||
}).toList(),
|
||||
]
|
||||
] //
|
||||
),
|
||||
),
|
||||
...controller.kr_subscribeService.allList.map((item) {
|
||||
return InkWell(
|
||||
onTap: () => _onNodeSelected(item),
|
||||
child: _kr_buildNodeListItem(context, item: item),
|
||||
);
|
||||
}).toList(),
|
||||
]
|
||||
] //
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -328,7 +355,8 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
style: KrAppTextStyle(fontSize: 14, color: Theme.of(context).textTheme.bodySmall?.color),
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14, color: Theme.of(context).textTheme.bodySmall?.color),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -344,17 +372,9 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 节点选中时的通用处理逻辑
|
||||
void _onNodeSelected(KROutboundItem item) {
|
||||
KRLogUtil.kr_i('Node selected: ${item.tag}');
|
||||
KRSingBoxImp.instance.kr_selectOutbound(item.tag);
|
||||
controller.homeController.kr_selectNode(item.tag);
|
||||
// 切换回主页的仪表盘视图
|
||||
controller.homeController.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
|
||||
/// 构建单个节点列表项的UI
|
||||
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
||||
Widget _kr_buildNodeListItem(BuildContext context,
|
||||
{required KROutboundItem item}) {
|
||||
return Container(
|
||||
key: ValueKey(item.id),
|
||||
decoration: BoxDecoration(
|
||||
@@ -369,71 +389,92 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
KRCountryFlag(countryCode: item.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
|
||||
KRCountryFlag(
|
||||
countryCode: item.country,
|
||||
width: 30.w,
|
||||
height: 20.w,
|
||||
isCircle: false,
|
||||
maintainSize: false),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
controller.homeController.kr_getCountryFullName(item.country),
|
||||
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
||||
? Container(
|
||||
margin: EdgeInsets.only(left: 4.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.w),
|
||||
decoration: BoxDecoration(
|
||||
color: krModernGreenLight.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
),
|
||||
child: Text(
|
||||
AppTranslations.kr_home.selected,
|
||||
style: KrAppTextStyle(fontSize: 10, color: krModernGreen, fontWeight: FontWeight.w500),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
],
|
||||
child: Obx(() {
|
||||
final isDebug = controller.isDebugMode.value;
|
||||
|
||||
final text = isDebug
|
||||
? '${controller.homeController.kr_getCountryFullName(item.country)} - ${item.tag}'
|
||||
: '${controller.homeController.kr_getCountryFullName(item.country)}';
|
||||
|
||||
return Text(
|
||||
text,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(height: 2.w),
|
||||
Obx(() {
|
||||
// 2. 获取用于显示的延迟值
|
||||
final int delay = _getDisplayDelay(controller, item);
|
||||
return Text(
|
||||
'${delay}ms',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: krModernGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Text(
|
||||
item.city,
|
||||
style: KrAppTextStyle(fontSize: 12, color: Theme.of(context).textTheme.bodySmall?.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
Obx(() {
|
||||
// 1. 获取延迟值和测速状态
|
||||
int displayDelay;
|
||||
bool isTesting = controller.homeController.kr_isLatency.value;
|
||||
|
||||
// 极简逻辑:只根据节点类型决定显示方式
|
||||
print(
|
||||
'🔄 _kr_buildNodeListItem节点: item.tag=${item.tag}, country=${item.country}');
|
||||
|
||||
// 普通节点,直接显示节点自身的速度
|
||||
displayDelay = item.urlTestDelay.value;
|
||||
print('🔄 _kr_buildNodeListItem使用普通节点延迟: $displayDelay');
|
||||
|
||||
// 2. 声明文本和颜色变量
|
||||
String delayText;
|
||||
Color delayColor;
|
||||
|
||||
// 3. 根据状态设置文本和颜色
|
||||
if (isTesting && displayDelay == 0) {
|
||||
delayText = '测速中...';
|
||||
delayColor = Colors.grey; // 测速时使用灰色
|
||||
} else if (displayDelay == 0) {
|
||||
delayText = '- ms'; // 未测速或初始状态
|
||||
delayColor = Colors.grey;
|
||||
} else if (displayDelay >= 3000) {
|
||||
delayText = AppTranslations.kr_home.timeout; // "超时"
|
||||
delayColor = Colors.red; // 超时状态使用红色
|
||||
} else {
|
||||
delayText = '${displayDelay}ms';
|
||||
// 正常延迟,根据快慢设置不同颜色
|
||||
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
|
||||
}
|
||||
|
||||
// 4. 返回最终的 Text 组件
|
||||
return Text(
|
||||
delayText,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: delayColor, // 使用动态计算出的颜色
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(width: 12.w),
|
||||
Obx(() {
|
||||
final selected =
|
||||
controller.homeController.kr_cutSeletedTag.value == item.tag;
|
||||
return selected
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -441,18 +482,6 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
|
||||
/// 构建国家列表项的UI
|
||||
Widget _kr_buildCountryListItem(BuildContext context, {required country}) {
|
||||
// 获取延迟颜色
|
||||
Color getLatencyColor(int delay) {
|
||||
if (delay == 0) {
|
||||
return Colors.transparent;
|
||||
} else if (delay < 500) {
|
||||
return krModernGreen;
|
||||
} else if (delay < 3000) {
|
||||
return Color(0xFFFFB700); // 使用更容易看清的黄色
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
return Container(
|
||||
key: ValueKey(country),
|
||||
decoration: BoxDecoration(
|
||||
@@ -467,45 +496,97 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
KRCountryFlag(countryCode: country.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
|
||||
KRCountryFlag(
|
||||
countryCode: country.country,
|
||||
width: 30.w,
|
||||
height: 20.w,
|
||||
isCircle: false,
|
||||
maintainSize: false),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.homeController.kr_getCountryFullName(country.country),
|
||||
style: KrAppTextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final int delay = getFastestNodeDelay(controller, country.outboundList);
|
||||
return Text(
|
||||
delay == 0
|
||||
? ''
|
||||
: delay >= 3000
|
||||
? AppTranslations.kr_home.timeout
|
||||
: '${delay}ms',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: getLatencyColor(delay),
|
||||
fontWeight: FontWeight.w500,
|
||||
// 1. 获取延迟值和测速状态
|
||||
int displayDelay =
|
||||
getFastestNodeDelay(controller, country.outboundList);
|
||||
bool isTesting = controller.homeController.kr_isLatency.value;
|
||||
|
||||
// 2. 声明文本和颜色变量
|
||||
String delayText;
|
||||
Color delayColor;
|
||||
|
||||
// 3. 根据状态设置文本和颜色
|
||||
if (isTesting && displayDelay == 0) {
|
||||
delayText = '测速中...';
|
||||
delayColor = Colors.grey; // 测速时使用灰色
|
||||
} else if (displayDelay == 0) {
|
||||
delayText = '- ms'; // 未测速或初始状态
|
||||
delayColor = Colors.grey;
|
||||
} else if (displayDelay >= 3000) {
|
||||
delayText = AppTranslations.kr_home.timeout; // "超时"
|
||||
delayColor = Colors.red; // 超时状态使用红色
|
||||
} else {
|
||||
delayText = '${displayDelay}ms';
|
||||
// 正常延迟,根据快慢设置不同颜色
|
||||
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
|
||||
}
|
||||
|
||||
// 4. 返回最终的 Text 组件
|
||||
// return Text(
|
||||
// delayText,
|
||||
// style: KrAppTextStyle(
|
||||
// fontSize: 10,
|
||||
// color: delayColor, // 使用动态计算出的颜色
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// );
|
||||
|
||||
return Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
// 使用 BoxDecoration 来设置形状和颜色
|
||||
decoration: BoxDecoration(
|
||||
color: delayColor, // ✅ 核心:使用之前逻辑计算出的动态颜色
|
||||
// shape: BoxShape.circle, // 使用圆形看起来更柔和,也可以用 BoxShape.rectangle
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(width: 12.w),
|
||||
Obx(() => controller.homeController.kr_coutryText.value == country.country
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
Obx(() {
|
||||
final selectedCountryField =
|
||||
controller.homeController.kr_coutryText.value;
|
||||
if (selectedCountryField == 'auto') {
|
||||
return KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
);
|
||||
}
|
||||
final selected = selectedCountryField == country.country;
|
||||
return selected
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,47 @@ class HINodePageView extends GetView<HINodeListController> {
|
||||
return HIBaseScaffold(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: Obx(() => controller.isDebugMode.value
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
controller.kr_resetDebugMode();
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bug_report,
|
||||
size: 12.w,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Text(
|
||||
'关闭调试',
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
),
|
||||
// 主要内容区域
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
// 模式切换器
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
||||
@@ -122,8 +159,7 @@ class HINodePageView extends GetView<HINodeListController> {
|
||||
padding: EdgeInsets.only(left: 60.w, right: 60.w, bottom: 90.w), // 为HIHelpEntrance预留空间
|
||||
// 2. 在 Padding 内部使用 EasyRefresh
|
||||
child: EasyRefresh(
|
||||
// 3. 绑定 onRefresh 回调
|
||||
onRefresh: controller.kr_subscribeService.kr_refreshAll,
|
||||
onRefresh: controller.kr_handleRefresh,
|
||||
// 4. 自定义 Header 样式
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
@@ -159,7 +195,12 @@ class HINodePageView extends GetView<HINodeListController> {
|
||||
}) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
onTap: () {
|
||||
// 处理模式按钮点击(用于调试模式激活)
|
||||
controller.kr_handleModeButtonClick();
|
||||
// 执行原有的点击逻辑
|
||||
onTap();
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||
@@ -186,4 +227,4 @@ class HINodePageView extends GetView<HINodeListController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,22 +222,22 @@ class HIUserInfoController extends GetxController {
|
||||
String iconName = 'devices';
|
||||
|
||||
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
|
||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeAndroid;
|
||||
deviceType = 'Android' ;// AppTranslations.kr_deviceManagement.deviceTypeAndroid;
|
||||
iconName = 'phone_android';
|
||||
} else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) {
|
||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIos;
|
||||
deviceType = 'iPhone'; // AppTranslations.kr_deviceManagement.deviceTypeIos;
|
||||
iconName = 'phone_iphone';
|
||||
} else if (userAgent.contains('iPad')) {
|
||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIpad;
|
||||
deviceType = 'iPad';// AppTranslations.kr_deviceManagement.deviceTypeIpad;
|
||||
iconName = 'tablet';
|
||||
} else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) {
|
||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeMacos;
|
||||
deviceType = 'Mac';// AppTranslations.kr_deviceManagement.deviceTypeMacos;
|
||||
iconName = 'desktop_mac';
|
||||
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
|
||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeWindows;
|
||||
deviceType = 'Windows'; //AppTranslations.kr_deviceManagement.deviceTypeWindows;
|
||||
iconName = 'computer';
|
||||
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
|
||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeLinux;
|
||||
deviceType = 'Linux';//AppTranslations.kr_deviceManagement.deviceTypeLinux;
|
||||
iconName = 'computer';
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,15 @@ import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_config.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart';
|
||||
|
||||
class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
const HIUserInfoView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDeviceLogin = KRAppRunData.getInstance().isDeviceLogin();
|
||||
final isDeviceLogin = KRAppRunData.getInstance().isDeviceLogin();
|
||||
return HIBaseScaffold(
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -68,75 +70,33 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(() {
|
||||
final account = KRAppRunData.getInstance().kr_account.value;
|
||||
final account = KRAppRunData.getInstance()
|
||||
.kr_account
|
||||
.value;
|
||||
final isDeviceLogin = account != null &&
|
||||
account.startsWith('9000');
|
||||
final accountText = (account ==null || isDeviceLogin)
|
||||
? '待绑定'
|
||||
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||
return Text(
|
||||
(account != null && account.isNotEmpty) ? account : '未绑定',
|
||||
accountText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 0.9,
|
||||
//height: 0.9,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final userId = KRAppRunData.getInstance().kr_userId.value;
|
||||
return Text(
|
||||
'ID: ${(userId != null && userId.toString().isNotEmpty) ? userId : '—'}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
String expiryText;
|
||||
|
||||
if (currentSubscribe == null) {
|
||||
expiryText = '尚未购买套餐';
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(now)) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
} else {
|
||||
final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
// 2. 拼接成最终的字符串
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiryText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
KRSubscriptionExpiryText(
|
||||
expireTimeProvider: () => controller
|
||||
.kr_subscribeService
|
||||
.kr_currentSubscribe
|
||||
.value
|
||||
?.expireTime,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -146,7 +106,8 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
SizedBox(height: 12.w),
|
||||
// 动态:如果已有账号,显示“修改密码”,否则显示“绑定邮箱”
|
||||
Obx(() {
|
||||
final isDeviceLogin = KRAppRunData.getInstance().isDeviceLogin();
|
||||
final isDeviceLogin =
|
||||
KRAppRunData.getInstance().isDeviceLogin();
|
||||
if (!isDeviceLogin) return SizedBox.shrink();
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
@@ -163,20 +124,24 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 0.w).copyWith(bottom: 10.w), // 在这里增加底部间距
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
margin: EdgeInsets.symmetric(horizontal: 0)
|
||||
.copyWith(bottom: 10), // 在这里增加底部间距
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22.w),
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border:
|
||||
Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
isDeviceLogin ? '绑定邮箱' : '修改密码',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -196,7 +161,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
if (controller.isLoading.value) {
|
||||
// 使用 Padding 在加载指示器上方添加间距
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 20.w),
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
@@ -204,18 +169,20 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 0),
|
||||
child: GridView.builder(
|
||||
// 1. 禁止 GridView 自身的滚动,因为它已经在 SingleChildScrollView 内部
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// 2. 让 GridView 根据内容自动调整高度
|
||||
shrinkWrap: true,
|
||||
// 3. 设置网格的配置
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, // 强制两列
|
||||
crossAxisSpacing: 10.w, // 水平间距
|
||||
mainAxisSpacing: 10.w, // 垂直间距
|
||||
childAspectRatio: 1.5, // 宽高比,需要微调以获得最佳视觉效果
|
||||
crossAxisSpacing: 10, // 水平间距
|
||||
mainAxisSpacing: 10, // 垂直间距
|
||||
mainAxisExtent: 76,
|
||||
// childAspectRatio: 1.5, // 宽高比,需要微调以获得最佳视觉效果
|
||||
),
|
||||
// 4. itemCount 和 itemBuilder 的逻辑保持不变
|
||||
itemCount: controller.devices.length + 1,
|
||||
@@ -229,13 +196,15 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
onDelete: (id) {
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w),
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
'请确认是否移除此设备?',
|
||||
style: KrAppTextStyle(
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontSize: 14.sp,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -247,8 +216,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
},
|
||||
onCancel: () {
|
||||
controller.deleteDevice(id);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -268,9 +236,11 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
),
|
||||
// 👇 核心改动 3: 将固定在底部的按钮放在 Expanded 外部
|
||||
Obx(() {
|
||||
if((KRAppRunData.getInstance().kr_account.value != null &&
|
||||
KRAppRunData.getInstance().kr_account.value!.startsWith('9000')))
|
||||
return SizedBox.shrink();
|
||||
if ((KRAppRunData.getInstance().kr_account.value != null &&
|
||||
KRAppRunData.getInstance()
|
||||
.kr_account
|
||||
.value!
|
||||
.startsWith('9000'))) return SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: Column(
|
||||
@@ -285,7 +255,10 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
'注销账号后,所有此账号内的剩余套餐和账户数据将被清空,无法找回。', // 1. 修改为正确的提示信息
|
||||
textAlign: TextAlign.left, // 文本居中
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -299,8 +272,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
Get.toNamed(Routes.KR_DELETE_ACCOUNT);
|
||||
},
|
||||
// 4. onConfirm 对应“返回”按钮的点击事件
|
||||
onConfirm: () {
|
||||
},
|
||||
onConfirm: () {},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@@ -308,7 +280,8 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w),
|
||||
border: Border.all(color: const Color(0xFFFF2ED1), width: 2),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFF2ED1), width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
@@ -331,7 +304,10 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
'确认要退出您的账号?', // 1. 修改为正确的提示信息
|
||||
textAlign: TextAlign.center, // 文本居中
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -340,15 +316,14 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
cancelText: '确认',
|
||||
confirmText: '返回',
|
||||
onCancel: () async {
|
||||
final currentDevice = controller.devices.firstWhere(
|
||||
(device) => device['is_current'] == true,
|
||||
final currentDevice =
|
||||
controller.devices.firstWhere(
|
||||
(device) => device['is_current'] == true,
|
||||
);
|
||||
final deviceId = currentDevice['id'] as String;
|
||||
await controller.deleteDevice(deviceId);
|
||||
},
|
||||
onConfirm: () {
|
||||
|
||||
},
|
||||
onConfirm: () {},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@@ -383,6 +358,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建“当前设备信息卡”的私有方法
|
||||
Widget _buildDeviceCard({
|
||||
required BuildContext context,
|
||||
@@ -403,13 +379,14 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
final deviceType = deviceInfo['type'] as String;
|
||||
final iconName = deviceInfo['icon'] as String;
|
||||
|
||||
return Stack( // 使用 Stack 来放置删除按钮
|
||||
return Stack(
|
||||
// 使用 Stack 来放置删除按钮
|
||||
children: [
|
||||
// 主体内容容器
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w), // 可以适当调整圆角
|
||||
borderRadius: BorderRadius.circular(24), // 可以适当调整圆角
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: Column(
|
||||
@@ -418,25 +395,25 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(_getIconData(iconName), color: Colors.white, size: 18.w),
|
||||
SizedBox(width: 6.w),
|
||||
Icon(_getIconData(iconName), color: Colors.white, size: 18),
|
||||
SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'设备:${_extractDeviceModel(userAgent)}',
|
||||
'设备:${deviceType}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp, //
|
||||
fontSize: 10, //
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'ID: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
|
||||
'SN: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 10.sp,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -490,44 +467,44 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
Widget _buildAddDeviceCard() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print('可用设备 2222');
|
||||
print('可用设备 ${controller.kr_subscribeService.kr_currentSubscribe}');
|
||||
// 在这里处理点击“添加设备”的逻辑
|
||||
final device_limit = AppConfig.getInstance().device_limit;
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w, bottom: 16.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Column 高度包裹内容
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 2. 第一个 Text 组件
|
||||
Text(
|
||||
'请使用邮箱登录新设备',
|
||||
style: KrAppTextStyle(
|
||||
color: Colors.black, // 可以使用稍浅
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w, bottom: 16.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Column 高度包裹内容
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 2. 第一个 Text 组件
|
||||
Text(
|
||||
'请使用绑定邮箱登录新设备',
|
||||
style: KrAppTextStyle(
|
||||
color: Colors.black, // 可以使用稍浅
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
SizedBox(height: 20.w),
|
||||
Obx(() {
|
||||
final current = controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
|
||||
// 如果 current 为 null,显示默认值 0
|
||||
final deviceLimit = current?.deviceLimit ?? 0;
|
||||
|
||||
return Text(
|
||||
'每个账号最多允许同时使用$deviceLimit台设备同时在线',
|
||||
style: KrAppTextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.w),
|
||||
Text(
|
||||
"每个账号最多允许同时使用${device_limit}台设备同时在线",
|
||||
style: KrAppTextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
confirmText: KRAppRunData.getInstance().isDeviceLogin() ? '前往' : null,
|
||||
cancelText: KRAppRunData.getInstance().isDeviceLogin() ? '取消' : null,
|
||||
onConfirm: () {
|
||||
Get.toNamed(
|
||||
Routes.MR_LOGIN,
|
||||
arguments: {'entry': 'bind_email'},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@@ -585,7 +562,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
);
|
||||
}
|
||||
|
||||
String _extractDeviceModel(String deviceName) {
|
||||
String _extractDeviceModel(String deviceName, String deviceType) {
|
||||
// 匹配括号内内容
|
||||
final RegExp regExp = RegExp(r'\((.*?)\)');
|
||||
final Match? match = regExp.firstMatch(deviceName);
|
||||
@@ -627,4 +604,4 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
return Icons.devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
|
||||
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
|
||||
import '../../../localization/app_translations.dart';
|
||||
|
||||
@@ -65,6 +66,11 @@ class KRDeleteAccountController extends GetxController {
|
||||
KRCommonUtil.kr_showToast(error.msg);
|
||||
},
|
||||
(success) {
|
||||
HIDialog.show(
|
||||
title: '*重要提示',
|
||||
message:
|
||||
'验证邮件已发送至邮箱,如无法找到,请检查垃圾邮件箱或营销邮件箱。',
|
||||
);
|
||||
kr_canSendCode.value = false;
|
||||
kr_countdown.value = 60;
|
||||
|
||||
|
||||
@@ -1,65 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart';
|
||||
import '../controllers/kr_delete_account_controller.dart';
|
||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||
|
||||
|
||||
class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
const KRDeleteAccountView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HIBaseScaffold(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildUserInfoSection(),
|
||||
SizedBox(height: 12.w),
|
||||
// 验证码输入框
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 250.h, // 2. 设置最小高度为 300 (使用 .h 适配屏幕)
|
||||
),
|
||||
// 3. 使用 Column 和 MainAxisAlignment.center 使其垂直居中
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
_buildVerificationCodeField(), // 4. 在这里调用,不修改其内部
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.requestDeleteAccount();
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w),
|
||||
border: Border.all(color: const Color(0xFFFF2ED1), width: 2),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildUserInfoSection(),
|
||||
SizedBox(height: 12.w),
|
||||
// 验证码输入框
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 250.h, // 2. 设置最小高度为 300 (使用 .h 适配屏幕)
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'注销账户',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFF2ED1),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
// 3. 使用 Column 和 MainAxisAlignment.center 使其垂直居中
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
_buildVerificationCodeField(), // 4. 在这里调用,不修改其内部
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.requestDeleteAccount();
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w),
|
||||
border:
|
||||
Border.all(color: const Color(0xFFFF2ED1), width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'注销账户',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFF2ED1),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
/*SizedBox(
|
||||
/*SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.requestDeleteAccount,
|
||||
@@ -79,15 +86,15 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
),
|
||||
),
|
||||
),*/
|
||||
SizedBox(height: 20.w),
|
||||
],
|
||||
SizedBox(height: 20.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildUserInfoSection() {
|
||||
return Row(
|
||||
children: [
|
||||
@@ -129,12 +136,14 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
||||
final userId = (KRAppRunData.getInstance().kr_userId.value ?? '').toString();
|
||||
final deviceId = KRAppRunData.getInstance().deviceId ?? '';
|
||||
|
||||
final account = KRAppRunData.getInstance().kr_account.value;
|
||||
final isDeviceLogin =
|
||||
account != null && account.startsWith('9000');
|
||||
final accountText = (account ==null || isDeviceLogin)
|
||||
? '待绑定'
|
||||
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||
return Text(
|
||||
'ID: ${userId.isNotEmpty ? userId : deviceId}',
|
||||
accountText,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
fontSize: 14.sp,
|
||||
@@ -142,51 +151,14 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
String expiryText;
|
||||
|
||||
if (currentSubscribe == null) {
|
||||
expiryText = '尚未购买套餐';
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(now)) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
} else {
|
||||
expiryText = '到期时间:${expireDateTime}';final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
// 2. 拼接成最终的字符串
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiryText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
KRSubscriptionExpiryText(
|
||||
expireTimeProvider: () => controller
|
||||
.kr_subscribeService.kr_currentSubscribe.value?.expireTime,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -197,10 +169,44 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
/// 构建注册页面的验证码输入框(包含间距)
|
||||
Widget _buildVerificationCodeField() {
|
||||
return SizedBox(
|
||||
height: 50.w, // 固定高度
|
||||
height: 50.w,
|
||||
child: TextField(
|
||||
controller: controller.kr_codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofillHints: const [AutofillHints.oneTimeCode],
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (value) {
|
||||
var v = value.replaceAll(RegExp(r"\s+"), "");
|
||||
if (v.length % 2 == 0 && v.isNotEmpty) {
|
||||
final half = v.length ~/ 2;
|
||||
final first = v.substring(0, half);
|
||||
final second = v.substring(half);
|
||||
if (first == second) {
|
||||
v = first;
|
||||
}
|
||||
}
|
||||
const maxLen = 6;
|
||||
if (v.length > maxLen) {
|
||||
v = v.substring(0, maxLen);
|
||||
}
|
||||
if (controller.kr_codeController.text != v) {
|
||||
controller.kr_codeController.value =
|
||||
controller.kr_codeController.value.copyWith(
|
||||
text: v,
|
||||
selection: TextSelection.collapsed(offset: v.length),
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
}
|
||||
if (v.isNotEmpty && (v.length >= 6)) {
|
||||
FocusScope.of(Get.context!).unfocus();
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
FocusScope.of(Get.context!).unfocus();
|
||||
},
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
@@ -212,37 +218,40 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
color: const Color(0xFFA6A6A6),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2),
|
||||
),
|
||||
isDense: true,
|
||||
suffixIconConstraints: BoxConstraints(
|
||||
maxHeight: 50.w, // 限制最大高度
|
||||
maxHeight: 50.w,
|
||||
maxWidth: 190.w,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
||||
child: Obx(() {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: controller.kr_canSendCode.value ? controller.kr_sendCode : null,
|
||||
onTap: controller.kr_canSendCode.value
|
||||
? controller.kr_sendCode
|
||||
: null,
|
||||
child: Container(
|
||||
width: 100.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: controller.kr_canSendCode.value
|
||||
color: controller.kr_canSendCode.value
|
||||
? Theme.of(Get.context!).primaryColor
|
||||
: const Color(0xFFD5D5D5),
|
||||
borderRadius: BorderRadius.circular(100.r), // 药丸状
|
||||
borderRadius: BorderRadius.circular(100.r),
|
||||
),
|
||||
child: Text(
|
||||
controller.kr_canSendCode.value
|
||||
@@ -253,7 +262,7 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
fontWeight: FontWeight.w600,
|
||||
color: controller.kr_canSendCode.value
|
||||
? Colors.black
|
||||
: const Color(0xFF464655) ,
|
||||
: const Color(0xFF464655),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
@@ -25,16 +26,18 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
final delay = controller.kr_currentNodeLatency.value;
|
||||
|
||||
// 🔧 关键: 强制读取两个 observable 确保追踪
|
||||
final _ = KRSingBoxImp.instance.kr_status.value; // 强制追踪
|
||||
final isConnected = controller.kr_isConnected.value; // 使用 controller 的状态
|
||||
final _ = KRSingBoxImp.instance.kr_status.value; // 强制追踪
|
||||
final isConnected = controller.kr_isConnected.value; // 使用 controller 的状态
|
||||
|
||||
// 再次读取状态用于判断
|
||||
final status = KRSingBoxImp.instance.kr_status.value;
|
||||
final isSwitching = status is SingboxStarting || status is SingboxStopping;
|
||||
final isSwitching =
|
||||
status is SingboxStarting || status is SingboxStopping;
|
||||
|
||||
print('🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
||||
print(
|
||||
'🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
||||
|
||||
final isShow = isConnected; // delay == -1 || isConnected;
|
||||
final isShow = delay == -1 || isConnected;
|
||||
|
||||
final Color buttonColor = Theme.of(context).primaryColor;
|
||||
final double screenWidth = Get.width;
|
||||
@@ -61,35 +64,51 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
maxWidth: buttonSize * 3,
|
||||
maxHeight: buttonSize * 3,
|
||||
child: ContinuousRippleEffect(
|
||||
color: Theme.of(context).primaryColor
|
||||
),
|
||||
color: Theme.of(context).primaryColor),
|
||||
),
|
||||
),
|
||||
|
||||
if (!isShow)
|
||||
Positioned(
|
||||
top: -20, // 距离顶部 10.w
|
||||
top: -20, // 距离顶部 10.w
|
||||
left: 115, // 距离右侧 10.w
|
||||
child: KrLocalImage(
|
||||
imageName: "hi-home-slogan",
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
),
|
||||
|
||||
/// ✅ 按钮主体
|
||||
Material(
|
||||
shape: const CircleBorder(),
|
||||
color: isShow ? Colors.transparent : Theme.of(context).primaryColor,
|
||||
color: isShow
|
||||
? Colors.transparent
|
||||
: Theme.of(context).primaryColor,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if(isSwitching) {
|
||||
print('🔵 Switch UI 正在更新,切换中点击了按钮: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
||||
HapticFeedback.lightImpact();
|
||||
if (isSwitching) {
|
||||
print(
|
||||
'🔵 Switch UI 正在更新,切换中点击了按钮: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
||||
return;
|
||||
}
|
||||
final hasValidSubscription =
|
||||
controller.kr_subscribeService.kr_availableSubscribes.isNotEmpty;
|
||||
final current = controller
|
||||
.kr_subscribeService.kr_currentSubscribe.value;
|
||||
bool hasValidSubscription = false;
|
||||
if (current != null) {
|
||||
DateTime? expire;
|
||||
try {
|
||||
expire = DateTime.parse(current.expireTime);
|
||||
} catch (_) {
|
||||
expire = null;
|
||||
}
|
||||
hasValidSubscription =
|
||||
expire != null && expire.isAfter(DateTime.now());
|
||||
}
|
||||
if (hasValidSubscription) {
|
||||
controller.kr_toggleSwitch(!controller.kr_isConnected.value);
|
||||
controller
|
||||
.kr_toggleSwitch(!controller.kr_isConnected.value);
|
||||
} else {
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
@@ -98,7 +117,10 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
'尚未购买套餐,请前往购买页面下单后即可开始极速网络体验',
|
||||
textAlign: TextAlign.left,
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -107,7 +129,8 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
cancelText: '取消',
|
||||
confirmText: '前往',
|
||||
onConfirm: () {
|
||||
GlobalOverlayService.instance.triggerSubscriptionAnimation();
|
||||
GlobalOverlayService.instance
|
||||
.triggerSubscriptionAnimation();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -119,7 +142,8 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (isShow)
|
||||
/// ✅ isShow = true,图片居中,文字贴近底部
|
||||
|
||||
/// ✅ isShow = true,图片居中,文字贴近底部
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
@@ -139,20 +163,21 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
controller.kr_connectText.value == '已连接'
|
||||
? '已连接\n点击断开'
|
||||
: controller.kr_connectText.value,
|
||||
key: ValueKey(controller.kr_connectText.value),
|
||||
key: ValueKey(
|
||||
controller.kr_connectText.value),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1
|
||||
),
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
/// ✅ isShow = false,恢复原本布局
|
||||
|
||||
/// ✅ isShow = false,恢复原本布局
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -167,7 +192,8 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
controller.kr_connectText.value == '已连接'
|
||||
? '已连接\n点击断开'
|
||||
: controller.kr_connectText.value,
|
||||
key: ValueKey(controller.kr_connectText.value),
|
||||
key:
|
||||
ValueKey(controller.kr_connectText.value),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
@@ -180,11 +206,8 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -193,6 +216,7 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ 呼吸式同心圆动画(最小圆固定270,其他圆在270~479间波动,初始直径差10%)
|
||||
/// ✅ 呼吸式同心圆动画(所有圆在270~470之间运动,起点相同但周期不同)
|
||||
class ContinuousRippleEffect extends StatefulWidget {
|
||||
@@ -223,10 +247,11 @@ class _ContinuousRippleEffectState extends State<ContinuousRippleEffect>
|
||||
// 动画周期差异:外层慢,内层快
|
||||
final duration = Duration(milliseconds: 3000 + i * 1200);
|
||||
|
||||
final controller =
|
||||
AnimationController(vsync: this, duration: duration)..repeat(reverse: true);
|
||||
final controller = AnimationController(vsync: this, duration: duration)
|
||||
..repeat(reverse: true);
|
||||
|
||||
final curved = CurvedAnimation(parent: controller, curve: Curves.easeInOutSine);
|
||||
final curved =
|
||||
CurvedAnimation(parent: controller, curve: Curves.easeInOutSine);
|
||||
_controllers.add(controller);
|
||||
_animations.add(curved);
|
||||
}
|
||||
@@ -291,4 +316,3 @@ class _ContinuousRippleEffectState extends State<ContinuousRippleEffect>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class CustomCircleRoute extends PageRouteBuilder {
|
||||
required this.child,
|
||||
required this.startOffset,
|
||||
required this.transitionColor,
|
||||
this.customDuration = const Duration(milliseconds: 1500),
|
||||
this.customDuration = const Duration(milliseconds: 250),
|
||||
}) : super(
|
||||
opaque: false,
|
||||
transitionDuration: customDuration,
|
||||
@@ -137,7 +137,7 @@ class _HISubscriptionCornerButtonState extends State<HISubscriptionCornerButton>
|
||||
if(currentRoute.isEmpty) return
|
||||
print('🔥 当前路由: $currentRoute');
|
||||
|
||||
if (currentRoute == Routes.KR_PURCHASE_MEMBERSHIP) {
|
||||
if (currentRoute == Routes.KR_PURCHASE_MEMBERSHIP || currentRoute == Routes.KR_ORDER_STATUS ) {
|
||||
print('🔥 已经在 KRPurchaseMembership 页面,无需跳转');
|
||||
return; // 已在目标页面,直接返回
|
||||
}
|
||||
@@ -162,7 +162,7 @@ class _HISubscriptionCornerButtonState extends State<HISubscriptionCornerButton>
|
||||
child: KRPurchaseMembershipView(),
|
||||
startOffset: startOffset,
|
||||
transitionColor: transitionColor,
|
||||
customDuration: const Duration(milliseconds: 1500),
|
||||
customDuration: const Duration(milliseconds: 500),
|
||||
),
|
||||
).then((_) {
|
||||
// 页面返回后恢复默认主题色
|
||||
|
||||
@@ -18,9 +18,22 @@ import 'kr_home_subscription_view.dart';
|
||||
import './hi_animated_connect_button.dart';
|
||||
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
||||
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
|
||||
class KRHomeView extends StatefulWidget {
|
||||
class KRHomeView extends StatefulWidget implements HasSwipeConfig {
|
||||
const KRHomeView({super.key});
|
||||
@override
|
||||
SwipeConfig get swipeConfig => SwipeConfig(
|
||||
enableLeft: true,
|
||||
enableRight: true,
|
||||
onLeft: () {
|
||||
Get.toNamed(Routes.HI_MENU);
|
||||
},
|
||||
onRight: () =>
|
||||
GlobalOverlayService.instance.triggerSubscriptionAnimation(),
|
||||
);
|
||||
|
||||
@override
|
||||
State<KRHomeView> createState() => _KRHomeViewState();
|
||||
@@ -35,8 +48,6 @@ class _KRHomeViewState extends State<KRHomeView> {
|
||||
controller = Get.find<KRHomeController>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -50,174 +61,186 @@ class _KRHomeViewState extends State<KRHomeView> {
|
||||
children: [
|
||||
HIBaseScaffold(
|
||||
showMenuButton: true,
|
||||
topContentAreaHeight: 80,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child:
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(40.w, 8.w, 0, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Hi快VPN-网在我在,网快我快',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
// 订阅信息
|
||||
Obx(() {
|
||||
// 1. 先获取订阅信息
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
|
||||
// 2. 准备一个 Widget 变量,用于存放最终要显示的内容
|
||||
Widget content;
|
||||
|
||||
// --- 定义统一的文本样式,并设置行高 ---
|
||||
final textStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2, // 您可以微调这个值来达到想要的视觉效果
|
||||
);
|
||||
|
||||
// 3. 开始条件判断
|
||||
if (currentSubscribe == null) {
|
||||
// --- 情况1: 没有任何订阅信息 ---
|
||||
content = Text(
|
||||
'尚未购买套餐',
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
// --- 情况2: 有订阅信息,需要判断是否过期 ---
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime = DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
// 日期格式解析失败
|
||||
}
|
||||
|
||||
if (expireDateTime != null && expireDateTime.isBefore(now)) {
|
||||
// --- 情况2.1: 订阅已过期 ---
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
// 使用换行符 \n 合并为单个 Text 组件
|
||||
content = Text(
|
||||
'您的套餐已于 $formattedExpireDate 到期\n请前往购买新套餐',
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
// --- 情况2.2: 订阅有效 ---
|
||||
final difference = expireDateTime?.difference(now);
|
||||
final remainingDaysText = (difference?.inDays ?? 0) > 0
|
||||
? '${difference!.inDays} 天'
|
||||
: '不足一天';
|
||||
// 使用换行符 \n 合并为单个 Text 组件
|
||||
content = Text(
|
||||
'套餐剩余:$remainingDaysText\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 返回包裹在公共 Container 中的最终内容
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.only(top: 8.h),
|
||||
child: content,
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8.h), // 添加一些垂直间距
|
||||
|
||||
// --- 自定义“闪连”Checkbox ---
|
||||
Obx(() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Row 的宽度包裹其内容
|
||||
children: [
|
||||
// --- 可点击的 Checkbox 区域 ---
|
||||
GestureDetector(
|
||||
// 点击方框和文字都可以切换状态
|
||||
onTap: () => controller.toggleQuickConnect(!controller.isQuickConnectEnabled.value),
|
||||
child: AbsorbPointer(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1. 自定义 Checkbox 的方框外观
|
||||
Container(
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
decoration: BoxDecoration(
|
||||
color: controller.isQuickConnectEnabled.value
|
||||
? Theme.of(context).primaryColor
|
||||
: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).primaryColor,
|
||||
width: 1.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: controller.isQuickConnectEnabled.value
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 14.w,
|
||||
color: Colors.black,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
// 2. “闪连”标签
|
||||
Text(
|
||||
'闪连',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
topContentAreaHeight: 80,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(40.w, 8.w, 0, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Hi快VPN-网在我在,网快我快',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
// --- 问号图标和点击弹窗区域 ---
|
||||
SizedBox(width: 6.w), // 文字和问号图标之间的间距
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HIDialog.show(
|
||||
title: '*闪连功能',
|
||||
message: '开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
|
||||
),
|
||||
// 订阅信息
|
||||
Obx(() {
|
||||
// 1. 先获取订阅信息
|
||||
final currentSubscribe = controller
|
||||
.kr_subscribeService.kr_currentSubscribe.value;
|
||||
|
||||
// 2. 准备一个 Widget 变量,用于存放最终要显示的内容
|
||||
Widget content;
|
||||
|
||||
// --- 定义统一的文本样式,并设置行高 ---
|
||||
final normalStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
);
|
||||
final highlightStyle = normalStyle.copyWith(
|
||||
color: const Color(0xFFFF00B7),
|
||||
);
|
||||
|
||||
// 3. 开始条件判断
|
||||
if (currentSubscribe == null) {
|
||||
// --- 情况1: 没有任何订阅信息 ---
|
||||
content = Text(
|
||||
'尚未购买套餐',
|
||||
style: highlightStyle,
|
||||
);
|
||||
} else {
|
||||
// --- 情况2: 有订阅信息,需要判断是否过期 ---
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
// 日期格式解析失败
|
||||
}
|
||||
|
||||
if (expireDateTime != null &&
|
||||
expireDateTime.isBefore(now)) {
|
||||
// --- 情况2.1: 订阅已过期 ---
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
// 使用换行符 \n 合并为单个 Text 组件
|
||||
content = Text(
|
||||
'您的套餐已于 $formattedExpireDate 到期\n请前往购买新套餐',
|
||||
style: highlightStyle,
|
||||
);
|
||||
},
|
||||
child: KrLocalImage(
|
||||
imageName: 'question-icon', // 图片名称
|
||||
imageType: ImageType.svg, // 图片类型
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
color: Colors.white, // 让图标颜色与边框协调
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
} else {
|
||||
// --- 情况2.2: 订阅有效 ---
|
||||
final difference =
|
||||
expireDateTime?.difference(now);
|
||||
final remainingDaysText =
|
||||
(difference?.inDays ?? 0) > 0
|
||||
? '${difference!.inDays} 天'
|
||||
: '不足一天';
|
||||
// 使用换行符 \n 合并为单个 Text 组件
|
||||
content = Text(
|
||||
'套餐剩余:$remainingDaysText\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
|
||||
style: normalStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 返回包裹在公共 Container 中的最终内容
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.only(top: 8.h),
|
||||
child: content,
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8.h), // 添加一些垂直间距
|
||||
|
||||
// --- 自定义“闪连”Checkbox ---
|
||||
Obx(() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Row 的宽度包裹其内容
|
||||
children: [
|
||||
// --- 可点击的 Checkbox 区域 ---
|
||||
GestureDetector(
|
||||
// 点击方框和文字都可以切换状态
|
||||
onTap: () => controller.toggleQuickConnect(
|
||||
!controller.isQuickConnectEnabled.value),
|
||||
child: AbsorbPointer(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1. 自定义 Checkbox 的方框外观
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: controller
|
||||
.isQuickConnectEnabled.value
|
||||
? Theme.of(context).primaryColor
|
||||
: Colors.transparent,
|
||||
border: Border.all(
|
||||
color:
|
||||
Theme.of(context).primaryColor,
|
||||
width: 1.5,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(4.r),
|
||||
),
|
||||
child: controller
|
||||
.isQuickConnectEnabled.value
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 14,
|
||||
color: Colors.black,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
// 2. “闪连”标签
|
||||
Text(
|
||||
'闪连',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// --- 问号图标和点击弹窗区域 ---
|
||||
SizedBox(width: 6), // 文字和问号图标之间的间距
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HIDialog.show(
|
||||
title: '*闪连功能',
|
||||
message:
|
||||
'开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
|
||||
);
|
||||
},
|
||||
child: KrLocalImage(
|
||||
imageName: 'question-icon', // 图片名称
|
||||
imageType: ImageType.svg, // 图片类型
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: Colors.white, // 让图标颜色与边框协调
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 将 HIAnimatedConnectButton 移到 HIBaseScaffold 外部,避免 SafeArea 约束
|
||||
HIAnimatedConnectButton(),
|
||||
],
|
||||
);
|
||||
),
|
||||
// 将 HIAnimatedConnectButton 移到 HIBaseScaffold 外部,避免 SafeArea 约束
|
||||
HIAnimatedConnectButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import '../controllers/kr_invite_controller.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
@@ -15,7 +16,6 @@ class KRInviteView extends GetView<KRInviteController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
|
||||
|
||||
return HIBaseScaffold(
|
||||
@@ -34,14 +34,16 @@ class KRInviteView extends GetView<KRInviteController> {
|
||||
children: [
|
||||
// 🟢 第一行:奖励说明
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
|
||||
width: double.infinity,
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(25.r),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min, // 由内容撑开
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: 'hi-home-logo',
|
||||
@@ -67,13 +69,14 @@ class KRInviteView extends GetView<KRInviteController> {
|
||||
SizedBox(height: 26.w),
|
||||
// 🟢 第二行:我的邀请码
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white, width: 2.0),
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
),
|
||||
child: Obx(
|
||||
() => Row(
|
||||
() => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
@@ -112,12 +115,27 @@ class KRInviteView extends GetView<KRInviteController> {
|
||||
),
|
||||
onPressed: () {
|
||||
if (controller.kr_referCode.value.isNotEmpty) {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: controller.kr_referCode.value),
|
||||
);
|
||||
KRCommonUtil.kr_showToast(
|
||||
AppTranslations.kr_invite.inviteCodeCopied,
|
||||
);
|
||||
final code = controller.kr_referCode.value;
|
||||
final text = '#您的好友邀请您使用Hi快网络加速器\n'
|
||||
'安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n'
|
||||
'$code\n'
|
||||
'您和您的好友将会分别获得3天免费时长\n\n'
|
||||
'点击此处进入下载页面\n'
|
||||
'或在浏览器输入hifastvpn.com下载#';
|
||||
if (GetPlatform.isIOS) {
|
||||
Share.share(
|
||||
text,
|
||||
subject: '直接分享Hi快VPN邀请链接',
|
||||
);
|
||||
} else {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: text),
|
||||
);
|
||||
KRCommonUtil.kr_showToast(
|
||||
AppTranslations.kr_invite.inviteCodeCopied,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -142,24 +160,29 @@ class KRInviteView extends GetView<KRInviteController> {
|
||||
TextField(
|
||||
controller: controller.otherInviteCodeController,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold),
|
||||
decoration: InputDecoration(
|
||||
hintText: '填入邀请人邀请码兑换免费时长...',
|
||||
hintStyle: const TextStyle(color: Color(0xFFA6A6A6)),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 22.w),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.white, width: 2.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.white, width: 2.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.white, width: 2.0),
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: 50.h),
|
||||
),
|
||||
@@ -198,10 +221,9 @@ class KRInviteView extends GetView<KRInviteController> {
|
||||
),
|
||||
|
||||
// 5. 将 HIHelpEntrance 作为 Stack 的直接子元素,它将恢复其原有的绝对定位能力
|
||||
if (!isKeyboardVisible)
|
||||
const HIHelpEntrance(),
|
||||
if (!isKeyboardVisible) const HIHelpEntrance(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,19 +305,33 @@ class KRLoginController extends GetxController
|
||||
|
||||
/// 发送验证码(仅支持邮箱)
|
||||
void kr_sendCode() async {
|
||||
final either = await KRAuthApi().kr_sendCode(
|
||||
accountController.text,
|
||||
2
|
||||
); // 重置密码验证码类型为3
|
||||
/*
|
||||
*
|
||||
* kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
|
||||
? 2 // 注册验证码类型为2
|
||||
: 3*/
|
||||
if (accountController.text.isEmpty) {
|
||||
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
int type;
|
||||
final check = await KRAuthApi().kr_isRegister(accountController.text);
|
||||
final result = check.fold((l) {
|
||||
KRCommonUtil.kr_showToast(l.msg);
|
||||
return null;
|
||||
}, (isRegistered) {
|
||||
print('isRegistered $isRegistered');
|
||||
return isRegistered ? 2 : 1;
|
||||
});
|
||||
if (result == null) return;
|
||||
type = result;
|
||||
|
||||
final either = await KRAuthApi().kr_sendCode(accountController.text, type);
|
||||
either.fold((l) {
|
||||
KRCommonUtil.kr_showToast(l.msg);
|
||||
}, (r) async {
|
||||
/// 开始倒计时
|
||||
HIDialog.show(
|
||||
title: '*重要提示',
|
||||
message:
|
||||
'验证邮件已发送至邮箱,如无法找到,请检查垃圾邮件箱或营销邮件箱。',
|
||||
);
|
||||
|
||||
_startCountdown();
|
||||
});
|
||||
}
|
||||
@@ -380,7 +394,14 @@ class KRLoginController extends GetxController
|
||||
);
|
||||
|
||||
either.fold(
|
||||
(l) => KRCommonUtil.kr_showToast(l.msg),
|
||||
(l) {
|
||||
HIDialog.show(
|
||||
message: l.msg,
|
||||
preventBackDismiss: true,
|
||||
confirmText: '确定',
|
||||
);
|
||||
return;
|
||||
},
|
||||
(r) async {
|
||||
_saveLoginData(r);
|
||||
KRCommonUtil.kr_showToast('登录成功');
|
||||
|
||||
@@ -12,6 +12,8 @@ import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class KRLoginView extends GetView<KRLoginController> {
|
||||
const KRLoginView({super.key});
|
||||
@@ -19,32 +21,40 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
|
||||
final isHideBack = (Get.arguments as Map<String, dynamic>?)?['is-back'] ?? false;
|
||||
final isHideBack =
|
||||
(Get.arguments as Map<String, dynamic>?)?['is-back'] ?? false;
|
||||
return HIBaseScaffold(
|
||||
showBack: !isHideBack,
|
||||
resizeToAvoidBottomInset: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w,),
|
||||
child: Column(
|
||||
children: [
|
||||
// Text(
|
||||
// '${controller.kr_loginStatus.value}',
|
||||
// style: TextStyle(color: Colors.white), // 使用 TextStyle()
|
||||
// ),
|
||||
SizedBox(height: 20.w),
|
||||
_buildUserInfoSection(),
|
||||
SizedBox(height: 12.w),
|
||||
_buildContentByEntry(),
|
||||
SizedBox(height: 100.w), // 为底部帮助按钮留出空间
|
||||
],
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 40.w,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Text(
|
||||
// '${controller.kr_loginStatus.value}',
|
||||
// style: TextStyle(color: Colors.white), // 使用 TextStyle()
|
||||
// ),
|
||||
SizedBox(height: 20.w),
|
||||
_buildUserInfoSection(),
|
||||
SizedBox(height: 12.w),
|
||||
_buildBindEmailLayout(),
|
||||
SizedBox(height: 100.w), // 为底部帮助按钮留出空间
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isKeyboardVisible)
|
||||
const HIHelpEntrance(),
|
||||
if (!isKeyboardVisible) const HIHelpEntrance(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -76,27 +86,15 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(() {
|
||||
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
||||
final email = KRAppRunData.getInstance().kr_account.value ?? '';
|
||||
return Text(
|
||||
email.isNotEmpty ? email : '',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 0.9,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
||||
final userId = (KRAppRunData.getInstance().kr_userId.value ?? '').toString();
|
||||
final deviceId = KRAppRunData.getInstance().deviceId ?? '';
|
||||
final account = KRAppRunData.getInstance().kr_account.value;
|
||||
final isDeviceLogin =
|
||||
account != null && account.startsWith('9000');
|
||||
final accountText = (account ==null || isDeviceLogin)
|
||||
? '待绑定'
|
||||
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||
|
||||
return Text(
|
||||
'ID: ${userId.isNotEmpty ? userId : deviceId}',
|
||||
accountText,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
fontSize: 14.sp,
|
||||
@@ -104,51 +102,14 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
String expiryText;
|
||||
|
||||
if (currentSubscribe == null) {
|
||||
expiryText = '尚未购买套餐';
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(now)) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
} else {
|
||||
expiryText = '到期时间:${expireDateTime}';final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
// 2. 拼接成最终的字符串
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiryText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
KRSubscriptionExpiryText(
|
||||
expireTimeProvider: () => controller
|
||||
.kr_subscribeService.kr_currentSubscribe.value?.expireTime,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -219,7 +180,7 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
// hintText: '确认密码',
|
||||
// isPassword: true,
|
||||
// ),
|
||||
SizedBox(height: 10.w),
|
||||
SizedBox(height: 10),
|
||||
// 👇 核心改动:使用新的验证码输入框
|
||||
_buildVerificationCodeField(),
|
||||
],
|
||||
@@ -265,7 +226,7 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
bool isPassword = false,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: 50.w, // 固定高度
|
||||
height: 50, // 固定高度
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
obscureText: isPassword,
|
||||
@@ -280,7 +241,8 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
color: const Color(0xFFA6A6A6),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w), // 调整为更圆的角
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
@@ -310,9 +272,44 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
// return SizedBox.shrink();
|
||||
// }
|
||||
return SizedBox(
|
||||
height: 50.w, // 固定高度
|
||||
height: 50, // 固定高度
|
||||
child: TextField(
|
||||
controller: controller.codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofillHints: const [AutofillHints.oneTimeCode],
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (value) {
|
||||
var v = value.replaceAll(RegExp("\\s+"), "");
|
||||
if (v.length % 2 == 0 && v.isNotEmpty) {
|
||||
final half = v.length ~/ 2;
|
||||
final first = v.substring(0, half);
|
||||
final second = v.substring(half);
|
||||
if (first == second) {
|
||||
v = first;
|
||||
}
|
||||
}
|
||||
const maxLen = 6;
|
||||
if (v.length > maxLen) {
|
||||
v = v.substring(0, maxLen);
|
||||
}
|
||||
if (controller.codeController.text != v) {
|
||||
controller.codeController.value =
|
||||
controller.codeController.value.copyWith(
|
||||
text: v,
|
||||
selection: TextSelection.collapsed(offset: v.length),
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
}
|
||||
if (v.isNotEmpty && (v.length >= 6)) {
|
||||
FocusScope.of(Get.context!).unfocus();
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
FocusScope.of(Get.context!).unfocus();
|
||||
},
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
@@ -324,7 +321,8 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
color: const Color(0xFFA6A6A6),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
@@ -344,14 +342,15 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
suffixIcon: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
||||
child: Obx(() {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: controller.kr_canSendCode.value ? () => controller.kr_sendCode() : null,
|
||||
onTap: controller.kr_canSendCode.value
|
||||
? () => controller.kr_sendCode()
|
||||
: null,
|
||||
child: Container(
|
||||
width: 100.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: controller.kr_canSendCode.value
|
||||
color: controller.kr_canSendCode.value
|
||||
? Theme.of(Get.context!).primaryColor
|
||||
: const Color(0xFFD5D5D5),
|
||||
borderRadius: BorderRadius.circular(100.r), // 药丸状
|
||||
@@ -363,7 +362,7 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
fontWeight: FontWeight.w600,
|
||||
color: controller.kr_canSendCode.value
|
||||
? Colors.black
|
||||
: const Color(0xFF464655) ,
|
||||
: const Color(0xFF464655),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -375,7 +374,6 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildSaveButton() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -383,7 +381,7 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 50.w,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(Get.context!).primaryColor,
|
||||
borderRadius: BorderRadius.circular(100.r),
|
||||
@@ -413,4 +411,4 @@ class KRLoginView extends GetView<KRLoginController> {
|
||||
controller.kr_check();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,77 +13,104 @@ import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart';
|
||||
import '../../../widgets/kr_simple_loading.dart';
|
||||
class KRMessageView extends GetView<KRMessageController> {
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart';
|
||||
|
||||
class KRMessageView extends GetView<KRMessageController>
|
||||
implements HasSwipeConfig {
|
||||
const KRMessageView({super.key});
|
||||
|
||||
@override
|
||||
SwipeConfig get swipeConfig =>
|
||||
SwipeConfig(enableLeft: true, onLeft: () => Get.back());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
return HIBaseScaffold(
|
||||
child: Stack(
|
||||
children: [
|
||||
// 主要内容区域
|
||||
Obx(() {
|
||||
return EasyRefresh(
|
||||
controller: controller.refreshController,
|
||||
onRefresh: controller.kr_onRefresh,
|
||||
onLoad: controller.kr_onLoadMore,
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
armedText: '释放刷新',
|
||||
readyText: '正在刷新...',
|
||||
processingText: '正在刷新...',
|
||||
processedText: '刷新成功',
|
||||
failedText: '刷新失败',
|
||||
messageText: '最后更新于 %T',
|
||||
textStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||
messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp),
|
||||
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
// 3. 添加 Footer 以显示上拉加载的UI提示
|
||||
footer: ClassicFooter(
|
||||
dragText: '上拉加载',
|
||||
armedText: '释放加载',
|
||||
readyText: '正在加载...',
|
||||
processingText: '正在加载...',
|
||||
processedText: '加载成功',
|
||||
failedText: '加载失败',
|
||||
noMoreText: '没有更多数据了',
|
||||
messageText: '最后更新于 %T',
|
||||
textStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||
messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp),
|
||||
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 0.w, bottom: 90.w), // 为HIHelpEntrance留出空间
|
||||
child: HiFixedScrollbar(
|
||||
controller: scrollController,
|
||||
isShowScrollbar: controller.kr_messages.length > 0,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
itemCount: controller.kr_messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = controller.kr_messages[index];
|
||||
final collapsibleItemData = HICollapsibleItem(
|
||||
title: message.title,
|
||||
content: [message.content],
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.w),
|
||||
child: HICollapsibleItemWidget(item: collapsibleItemData),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 主要内容区域
|
||||
Obx(() {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 90.w),
|
||||
child: EasyRefresh(
|
||||
controller: controller.refreshController,
|
||||
onRefresh: controller.kr_onRefresh,
|
||||
onLoad: controller.kr_onLoadMore,
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
armedText: '释放刷新',
|
||||
readyText: '正在刷新...',
|
||||
processingText: '正在刷新...',
|
||||
processedText: '刷新成功',
|
||||
failedText: '刷新失败',
|
||||
messageText: '最后更新于 %T',
|
||||
textStyle:
|
||||
TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||
messageStyle: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12.sp),
|
||||
iconTheme: IconThemeData(
|
||||
color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
footer: ClassicFooter(
|
||||
dragText: '上拉加载',
|
||||
armedText: '释放加载',
|
||||
readyText: '正在加载...',
|
||||
processingText: '正在加载...',
|
||||
processedText: '加载成功',
|
||||
failedText: '加载失败',
|
||||
noMoreText: '没有更多数据了',
|
||||
messageText: '最后更新于 %T',
|
||||
textStyle:
|
||||
TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||
messageStyle: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12.sp),
|
||||
iconTheme: IconThemeData(
|
||||
color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 0.w),
|
||||
child: HiFixedScrollbar(
|
||||
controller: scrollController,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
padding:
|
||||
EdgeInsets.only(left: 40.w - 20, right: 40.w),
|
||||
itemCount: controller.kr_messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = controller.kr_messages[index];
|
||||
final collapsibleItemData = HICollapsibleItem(
|
||||
title: message.title,
|
||||
content: [message.content],
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.w),
|
||||
child: HICollapsibleItemWidget(
|
||||
item: collapsibleItemData),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
},),
|
||||
// 底部帮助入口
|
||||
const HIHelpEntrance(),
|
||||
],
|
||||
],
|
||||
);
|
||||
}),
|
||||
// 底部帮助入口
|
||||
const HIHelpEntrance(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||
import '../controllers/kr_order_status_controller.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
|
||||
/// 订单状态视图
|
||||
class KROrderStatusView extends GetView<KROrderStatusController> {
|
||||
@@ -13,66 +15,36 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
return HIBaseScaffold(
|
||||
showBackgroundImage: false,
|
||||
title: AppTranslations.kr_orderStatus.title,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios,
|
||||
size: 20.r,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
AppTranslations.kr_orderStatus.title,
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color.fromRGBO(23, 151, 255, 0.15),
|
||||
Color.fromRGBO(23, 151, 255, 0.05),
|
||||
],
|
||||
stops: [0.0, 0.28],
|
||||
),
|
||||
),
|
||||
child: Obx(
|
||||
() => SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 60.h),
|
||||
// 状态图标
|
||||
_buildStatusIcon(),
|
||||
SizedBox(height: 32.h),
|
||||
// 状态文本
|
||||
_buildStatusText(),
|
||||
SizedBox(height: 16.h),
|
||||
// 描述文本
|
||||
_buildDescriptionText(),
|
||||
SizedBox(height: 32.h),
|
||||
// 操作按钮(支付成功时显示)
|
||||
if (controller.kr_isPaymentSuccess.value)
|
||||
_buildSuccessButtons(),
|
||||
const Spacer(),
|
||||
],
|
||||
topContentAreaHeight: 110,
|
||||
child: Obx(
|
||||
() => Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.w),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 状态文本
|
||||
_buildStatusText(),
|
||||
SizedBox(height: 16.h),
|
||||
// 描述文本
|
||||
_buildDescriptionText(),
|
||||
SizedBox(height: 32.h),
|
||||
// 操作按钮(支付成功时显示)
|
||||
if (controller.kr_isPaymentSuccess.value)
|
||||
_buildSuccessButtons(),
|
||||
SizedBox(height: 100.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HIHelpEntrance(isLight: false),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -134,7 +106,7 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
|
||||
+1
-1
@@ -621,7 +621,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
return '';
|
||||
}
|
||||
// 计算折扣百分比(例如:97% 显示为 -3%)
|
||||
final discountPercent = 100 - discount.kr_discount;
|
||||
final discountPercent = (100 - discount.kr_discount).ceil();
|
||||
return '${discountPercent}%';
|
||||
}
|
||||
return '';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -170,13 +170,16 @@ class KRSplashController extends GetxController {
|
||||
print('📞 准备调用 KRSiteConfigService().initialize()...');
|
||||
final success = await KRSiteConfigService().initialize();
|
||||
final crispId = await KRSiteConfigService().getCrispId();
|
||||
final device_limit = await KRSiteConfigService().getDeviceLimit();
|
||||
print('📞 KRSiteConfigService().initialize() 返回: $success');
|
||||
|
||||
if (success) {
|
||||
final config = AppConfig.getInstance();
|
||||
config.kr_website_id = crispId;
|
||||
config.device_limit = device_limit;
|
||||
print('📞 KRSiteConfigService().initialize() 返回: $crispId');
|
||||
print('AppConfig website_id 已更新为: ${config.kr_website_id}');
|
||||
print('AppConfig device_limit 已更新为: ${config.device_limit}');
|
||||
|
||||
// print('[SPLASH_TIMING] ✅ 网站配置初始化成功');
|
||||
// await _kr_checkAndPerformDeviceLogin();
|
||||
@@ -285,7 +288,7 @@ class KRSplashController extends GetxController {
|
||||
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
||||
print('⏱️ 初始化超时,直接跳转到主页');
|
||||
// 超时后直接跳转到主页,让用户可以手动重试
|
||||
// Get.offAllNamed(Routes.KR_MAIN);
|
||||
// Get.offAllNamed(Routes.KR_HOME);
|
||||
HIDialog.show(
|
||||
message: '初始化超时,请检查网络或重试',
|
||||
confirmText: '重试',
|
||||
@@ -484,7 +487,7 @@ class KRSplashController extends GetxController {
|
||||
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
||||
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
||||
await _executeMinimalInitialization();
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,7 +496,7 @@ class KRSplashController extends GetxController {
|
||||
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
||||
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
||||
await _executeMinimalInitialization();
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -582,7 +585,7 @@ class KRSplashController extends GetxController {
|
||||
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
||||
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
||||
await _executeMinimalInitialization();
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +729,7 @@ class KRSplashController extends GetxController {
|
||||
kr_isLoading.value = false;
|
||||
|
||||
// 直接跳转到主页
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
}
|
||||
|
||||
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
||||
|
||||
@@ -27,9 +27,7 @@ class BaseResponse<T> {
|
||||
final nonce = dataMap['time'] ?? "";
|
||||
|
||||
// 判断是否需要解密:根据站点配置的 enable_security 字段
|
||||
final shouldDecrypt = KRSiteConfigService().isDeviceSecurityEnabled();
|
||||
|
||||
if (shouldDecrypt && cipherText.isNotEmpty && nonce.isNotEmpty) {
|
||||
if (cipherText.isNotEmpty && nonce.isNotEmpty) {
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
print('═══════════════════════════════════════');
|
||||
|
||||
@@ -128,7 +128,8 @@ class HttpUtil {
|
||||
void updateBaseUrl() {
|
||||
String newBaseUrl = AppConfig.getInstance().baseUrl;
|
||||
if (_dio.options.baseUrl != newBaseUrl) {
|
||||
KRLogUtil.kr_i('🔄 更新baseUrl: ${_dio.options.baseUrl} -> $newBaseUrl', tag: 'HttpUtil');
|
||||
KRLogUtil.kr_i('🔄 更新baseUrl: ${_dio.options.baseUrl} -> $newBaseUrl',
|
||||
tag: 'HttpUtil');
|
||||
_dio.options.baseUrl = newBaseUrl;
|
||||
}
|
||||
}
|
||||
@@ -177,7 +178,7 @@ class HttpUtil {
|
||||
try {
|
||||
// 每次请求前更新baseUrl,确保使用最新的域名
|
||||
updateBaseUrl();
|
||||
|
||||
|
||||
if (isShowLoading) {
|
||||
KRCommonUtil.kr_showLoading();
|
||||
}
|
||||
@@ -240,7 +241,9 @@ class HttpUtil {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final userId = _kr_parseUserIdFromToken(KRAppRunData().kr_token.toString());
|
||||
|
||||
final userId =
|
||||
_kr_parseUserIdFromToken(KRAppRunData().kr_token.toString());
|
||||
// 调试:打印请求头
|
||||
KRLogUtil.kr_i('🔍 请求头: $headers', tag: 'HttpUtil');
|
||||
KRLogUtil.kr_i('🔍 请求userId: $userId', tag: 'HttpUtil');
|
||||
@@ -326,18 +329,22 @@ class HttpUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
return BaseResponse<T>.fromJson({
|
||||
'code': code,
|
||||
'msg': msg,
|
||||
'data': <String, dynamic>{}
|
||||
});
|
||||
if (err.type == DioExceptionType.unknown) {
|
||||
final _pathOnly = (err.requestOptions.path.isNotEmpty)
|
||||
? err.requestOptions.path
|
||||
: err.requestOptions.uri.path;
|
||||
msg = '${msg.isNotEmpty ? msg : 'unknown'} ($_pathOnly)';
|
||||
KRCommonUtil.kr_showToast('请求失败($_pathOnly)', timeout: 3500);
|
||||
}
|
||||
return BaseResponse<T>.fromJson(
|
||||
{'code': code, 'msg': msg, 'data': <String, dynamic>{}});
|
||||
} catch (e) {
|
||||
if (isShowLoading) {
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
}
|
||||
return BaseResponse<T>.fromJson({
|
||||
'code': -90000,
|
||||
'msg': e.toString(),
|
||||
'msg': '${e.toString()} (${path})',
|
||||
'data': <String, dynamic>{}
|
||||
});
|
||||
}
|
||||
@@ -362,7 +369,8 @@ class MyInterceptor extends Interceptor {
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
print('<<< Response │ ${response.requestOptions.method} │ ${response.statusCode} ${response.statusMessage} │ ${response.requestOptions.uri}');
|
||||
print(
|
||||
'<<< Response │ ${response.requestOptions.method} │ ${response.statusCode} ${response.statusMessage} │ ${response.requestOptions.uri}');
|
||||
}
|
||||
if (response.data != null) {
|
||||
if (kDebugMode) {
|
||||
@@ -375,7 +383,8 @@ class MyInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
print('<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||||
print(
|
||||
'<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('Error Type: ${err.type}');
|
||||
@@ -396,6 +405,8 @@ class MyInterceptor extends Interceptor {
|
||||
|
||||
/// 自定义简洁 HTTP 拦截器(无边框符号)
|
||||
class _KRSimpleHttpInterceptor extends Interceptor {
|
||||
static String? _lastPath;
|
||||
static int _lastTsMs = 0;
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
@@ -454,7 +465,8 @@ class _KRSimpleHttpInterceptor extends Interceptor {
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
print('<<< Response │ ${response.requestOptions.method} │ ${response.statusCode} ${response.statusMessage} │ ${response.requestOptions.uri}');
|
||||
print(
|
||||
'<<< Response │ ${response.requestOptions.method} │ ${response.statusCode} ${response.statusMessage} │ ${response.requestOptions.uri}');
|
||||
}
|
||||
if (response.data != null) {
|
||||
if (kDebugMode) {
|
||||
@@ -514,7 +526,8 @@ class _KRSimpleHttpInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
print('<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||||
print(
|
||||
'<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('Error Type: ${err.type}');
|
||||
@@ -529,6 +542,19 @@ class _KRSimpleHttpInterceptor extends Interceptor {
|
||||
print('Response Data: ${err.response?.data}');
|
||||
}
|
||||
}
|
||||
if (err.type == DioExceptionType.unknown) {
|
||||
final path = (err.requestOptions.path.isNotEmpty)
|
||||
? err.requestOptions.path
|
||||
: err.requestOptions.uri.path;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (!(_lastPath == path && (now - _lastTsMs) < 2000)) {
|
||||
_lastPath = path;
|
||||
_lastTsMs = now;
|
||||
KRCommonUtil.kr_showToast('请求失败($path)', timeout: 3500);
|
||||
}
|
||||
KRLogUtil.kr_e('请求失败($path)',
|
||||
tag: 'HttpUtil', error: err, stackTrace: err.stackTrace);
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import '../modules/kr_splash/views/kr_splash_view.dart';
|
||||
import '../modules/kr_device_management/bindings/kr_device_management_binding.dart';
|
||||
import '../modules/kr_device_management/views/kr_device_management_view.dart';
|
||||
import 'package:kaer_with_panels/app/routes/transitions/slide_transparent_transition.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/swipe/swipe_wrapper.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
@@ -54,20 +55,23 @@ class AppPages {
|
||||
static final routes = [
|
||||
GetPage(
|
||||
name: Routes.KR_SPLASH,
|
||||
page: () => const KRSplashView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRSplashView()),
|
||||
binding: KRSplashBinding(),
|
||||
popGesture: false,
|
||||
transition: Transition.fade,
|
||||
transitionDuration: const Duration(milliseconds: 500),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_MAIN,
|
||||
page: () => const KRMainView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRMainView()),
|
||||
binding: KRMainBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_HOME,
|
||||
page: () => KRHomeView(),
|
||||
page: () => SwipeWrapper.detect(() => KRHomeView()),
|
||||
binding: KRHomeBinding(),
|
||||
popGesture: false,
|
||||
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
|
||||
customTransition: SlideOutOnlyTransition(
|
||||
direction: SlideDirection.leftToRight,
|
||||
@@ -76,8 +80,9 @@ class AppPages {
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.HI_MENU,
|
||||
page: () => const HIMenuView(),
|
||||
page: () => SwipeWrapper.detect(() => const HIMenuView()),
|
||||
binding: HIMenuBinding(),
|
||||
popGesture: false,
|
||||
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
|
||||
customTransition: ContextAwareSlideTransition(
|
||||
targetRoute: _Paths.HI_MENU,
|
||||
@@ -86,90 +91,107 @@ class AppPages {
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.MR_LOGIN,
|
||||
page: () => const KRLoginView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRLoginView()),
|
||||
binding: MrLoginBinding(),
|
||||
popGesture: false,
|
||||
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_SETTING,
|
||||
page: () => const KRSettingView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRSettingView()),
|
||||
binding: KRSettingBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_USER_INFO,
|
||||
page: () => const KRUserInfoView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRUserInfoView()),
|
||||
binding: KRUserInfoBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_INVITE,
|
||||
page: () => const KRInviteView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRInviteView()),
|
||||
binding: KRInviteBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_STATISTICS,
|
||||
page: () => const KRStatisticsView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRStatisticsView()),
|
||||
binding: KRStatisticsBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_LANGUAGE_SELECTOR,
|
||||
page: () => const KRLanguageSelectorView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRLanguageSelectorView()),
|
||||
binding: KRLanguageSelectorBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_COUNTRY_SELECTOR,
|
||||
page: () => const KRCountrySelectorView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRCountrySelectorView()),
|
||||
binding: KRCountrySelectorBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_PURCHASE_MEMBERSHIP,
|
||||
page: () => const KRPurchaseMembershipView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRPurchaseMembershipView()),
|
||||
binding: KRPurchaseMembershipBinding(),
|
||||
popGesture: false,
|
||||
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_MESSAGE,
|
||||
page: () => const KRMessageView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRMessageView()),
|
||||
binding: KrMessageBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_DELETE_ACCOUNT,
|
||||
page: () => const KRDeleteAccountView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRDeleteAccountView()),
|
||||
binding: KrDeleteAccountBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.KR_WEBVIEW,
|
||||
page: () => const KRWebView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRWebView()),
|
||||
binding: KRWebViewBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.KR_ORDER_STATUS,
|
||||
page: () => const KROrderStatusView(),
|
||||
page: () => SwipeWrapper.detect(() => const KROrderStatusView()),
|
||||
binding: KROrderStatusBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_CRISP,
|
||||
page: () => const KRCrispView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRCrispView()),
|
||||
binding: KRCrispBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.KR_DEVICE_MANAGEMENT,
|
||||
page: () => const KRDeviceManagementView(),
|
||||
page: () => SwipeWrapper.detect(() => const KRDeviceManagementView()),
|
||||
binding: KRDeviceManagementBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.HI_NODE_LIST,
|
||||
page: () => const HINodePageView(),
|
||||
page: () => SwipeWrapper.detect(() => const HINodePageView()),
|
||||
binding: HiNodeListBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.HI_HELP,
|
||||
page: () => const HIHelpView(),
|
||||
page: () => SwipeWrapper.detect(() => const HIHelpView()),
|
||||
binding: HIHelpBinding(),
|
||||
popGesture: false,
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.HI_USER_INFO,
|
||||
page: () => const HIUserInfoView(),
|
||||
page: () => SwipeWrapper.detect(() => const HIUserInfoView()),
|
||||
binding: HIUserInfoBinding(),
|
||||
popGesture: false,
|
||||
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
|
||||
),
|
||||
];
|
||||
|
||||
@@ -110,7 +110,7 @@ class SlideTransparentTransition extends CustomTransition {
|
||||
|
||||
SlideTransparentTransition({
|
||||
this.direction = SlideDirection.rightToLeft,
|
||||
this.duration = const Duration(milliseconds: 4000),
|
||||
this.duration = const Duration(milliseconds: 1000),
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'slide_transparent_transition.dart';
|
||||
|
||||
/// 全局页面过渡动画配置类
|
||||
class TransitionConfig {
|
||||
static Duration _defaultDuration = const Duration(milliseconds: 350);
|
||||
static Duration _defaultDuration = const Duration(milliseconds: 150);
|
||||
static const SlideDirection _defaultDirection = SlideDirection.rightToLeft;
|
||||
static const Curve _defaultCurve = Curves.easeOutCubic;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// 接口名称
|
||||
abstract class Api {
|
||||
/// 游客登录查看是否已经注册
|
||||
static const String kr_isRegister = "/v1/app/auth/check";
|
||||
static const String kr_isRegister = "/v1/auth/check";
|
||||
/// 判断邮箱和当前设备是否已存在订阅
|
||||
static const String kr_checkSubscription = "/v1/public/user/subscribe_status";
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class KRAuthApi {
|
||||
|
||||
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
|
||||
.request<KRIsRegister>(Api.kr_isRegister, data,
|
||||
method: HttpMethod.POST, isShowLoading: true);
|
||||
method: HttpMethod.GET, isShowLoading: true);
|
||||
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
@@ -298,7 +298,7 @@ class KRAuthApi {
|
||||
|
||||
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
|
||||
.request<dynamic>(Api.kr_deleteAccount, data,
|
||||
method: HttpMethod.DELETE, isShowLoading: true);
|
||||
method: HttpMethod.POST, isShowLoading: true);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
|
||||
@@ -78,7 +78,7 @@ class GlobalOverlayService extends GetxService {
|
||||
// 2️⃣ money-icon,独立定位,固定在屏幕右上角
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 8,
|
||||
right: (radius - (statusBarHeight / 2)) / 2,
|
||||
right: ((radius - (statusBarHeight / 2)) / 2) + 3,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent, // 让区域可响应点击
|
||||
onTap: () {
|
||||
@@ -88,15 +88,9 @@ class GlobalOverlayService extends GetxService {
|
||||
},
|
||||
child: IgnorePointer(
|
||||
ignoring: false, // 不阻断事件
|
||||
child: SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: KrLocalImage(
|
||||
imageName: 'money-icon',
|
||||
width: 44,
|
||||
height: 44,
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
child: KrLocalImage(
|
||||
imageName: 'money-icon',
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -341,6 +341,11 @@ class KRSiteConfigService extends ChangeNotifier {
|
||||
return _siteConfig?.site.crispId ?? '0';
|
||||
}
|
||||
|
||||
/// 获取设备限制
|
||||
String getDeviceLimit() {
|
||||
return _siteConfig?.site.deviceLimit ?? '0';
|
||||
}
|
||||
|
||||
/// 重置配置
|
||||
void reset() {
|
||||
_siteConfig = null;
|
||||
|
||||
@@ -661,6 +661,7 @@ class KRSubscribeService {
|
||||
|
||||
// 保存配置
|
||||
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
||||
|
||||
// 更新试用和订阅状态
|
||||
_kr_updateSubscribeStatus();
|
||||
|
||||
@@ -722,6 +723,7 @@ class KRSubscribeService {
|
||||
|
||||
// 保存配置
|
||||
KRSingBoxImp.instance.kr_saveOutbounds([]);
|
||||
|
||||
}
|
||||
|
||||
/// 获取当前订阅
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ class KRCommonUtil {
|
||||
/// 提示 meesage: 提示内容, toastPosition: 提示显示的位置, timeout: 显示时间(毫秒)
|
||||
static kr_showToast(String message,
|
||||
{KRToastPosition toastPosition = KRToastPosition.center,
|
||||
int timeout = 1500}) {
|
||||
int timeout = 2500}) {
|
||||
KRToast.kr_showToast(
|
||||
message,
|
||||
position: toastPosition,
|
||||
|
||||
@@ -15,7 +15,8 @@ enum KRCountry {
|
||||
ru('俄罗斯'),
|
||||
id('印度尼西亚'),
|
||||
tr('土耳其'),
|
||||
br('巴西');
|
||||
br('巴西'),
|
||||
other('全局代理'); // ✅ 新增:全局代理模式(所有流量走代理,不添加国家直连规则)
|
||||
|
||||
final String kr_name;
|
||||
const KRCountry(this.kr_name);
|
||||
@@ -30,7 +31,7 @@ enum KRCountry {
|
||||
static KRCountry? kr_fromCode(String code) {
|
||||
try {
|
||||
return KRCountry.values.firstWhere(
|
||||
(country) => country.kr_code == code.toLowerCase(),
|
||||
(country) => country.kr_code == code.toLowerCase(),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -53,7 +54,7 @@ class KRCountryUtil {
|
||||
static Future<void> kr_init() async {
|
||||
try {
|
||||
final String? kr_savedCountry =
|
||||
await _kr_storage.kr_readData(key: _kr_countryKey);
|
||||
await _kr_storage.kr_readData(key: _kr_countryKey);
|
||||
if (kr_savedCountry != null) {
|
||||
final KRCountry? kr_country = KRCountry.kr_fromCode(kr_savedCountry);
|
||||
if (kr_country != null) {
|
||||
@@ -66,7 +67,7 @@ class KRCountryUtil {
|
||||
} else {
|
||||
kr_currentCountry.value = KRCountry.cn;
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
KRLogUtil.kr_e('初始化国家设置失败: $err', tag: 'CountryUtil');
|
||||
kr_currentCountry.value = KRCountry.cn;
|
||||
@@ -106,9 +107,9 @@ class KRCountryUtil {
|
||||
return kr_currentCountry.value.kr_code;
|
||||
}
|
||||
|
||||
static String kr_getCurrentCountryName() {
|
||||
static String kr_getCurrentCountryName() {
|
||||
return kr_getCountryName(kr_currentCountry.value);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// 获取国家名称
|
||||
@@ -131,12 +132,14 @@ class KRCountryUtil {
|
||||
return AppTranslations.kr_country.tr;
|
||||
case KRCountry.br:
|
||||
return AppTranslations.kr_country.br;
|
||||
case KRCountry.other:
|
||||
return '全局代理'; // ✅ 新增:全局代理
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有支持的国家列表
|
||||
static List<KRCountry> kr_getSupportedCountries() {
|
||||
if (AppConfig().kr_is_daytime == false) {
|
||||
if (AppConfig().kr_is_daytime == false) {
|
||||
return KRCountry.values.where((element) => element != KRCountry.cn).toList();
|
||||
}
|
||||
return KRCountry.values;
|
||||
@@ -146,9 +149,9 @@ class KRCountryUtil {
|
||||
static List<Map<String, String>> kr_getCountryInfoList() {
|
||||
return KRCountry.values
|
||||
.map((country) => {
|
||||
'code': country.kr_code,
|
||||
'name': country.kr_countryName,
|
||||
})
|
||||
'code': country.kr_code,
|
||||
'name': country.kr_countryName,
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +1,151 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'kr_log_util.dart';
|
||||
|
||||
/// 延迟测试工具类
|
||||
/// 提供真实的 TCP 连接延迟测试功能
|
||||
/// 真正的节点延迟测试工具
|
||||
class KRLatencyTester {
|
||||
/// 测试单个节点的延迟
|
||||
///
|
||||
/// 参数:
|
||||
/// - host: 主机地址
|
||||
/// - port: 端口号
|
||||
/// - timeout: 超时时间(毫秒)
|
||||
///
|
||||
/// 返回:
|
||||
/// - 延迟时间(毫秒),如果失败返回 65535
|
||||
static Future<int> testNode({
|
||||
/// TCP 连接测试延迟(真实测试)
|
||||
/// 返回延迟毫秒数,失败返回 65535
|
||||
static Future<int> testTcpLatency({
|
||||
required String host,
|
||||
required int port,
|
||||
int timeout = 5000,
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
Socket? socket;
|
||||
final stopwatch = Stopwatch();
|
||||
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
final socket = await Socket.connect(
|
||||
KRLogUtil.kr_i('🔌 开始测试: $host:$port', tag: 'LatencyTester');
|
||||
|
||||
stopwatch.start();
|
||||
|
||||
// 尝试 TCP 连接
|
||||
socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: Duration(milliseconds: timeout),
|
||||
).timeout(Duration(milliseconds: timeout));
|
||||
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
// 立即关闭连接
|
||||
await socket.close();
|
||||
socket.destroy();
|
||||
|
||||
|
||||
final latency = stopwatch.elapsedMilliseconds;
|
||||
KRLogUtil.kr_i('✅ 延迟测试成功: $host:$port = ${latency}ms', tag: 'KRLatencyTester');
|
||||
|
||||
|
||||
KRLogUtil.kr_i('✅ 测试成功: $host:$port - ${latency}ms', tag: 'LatencyTester');
|
||||
|
||||
return latency;
|
||||
|
||||
} on SocketException catch (e) {
|
||||
stopwatch.stop();
|
||||
KRLogUtil.kr_w('❌ 连接失败: $host:$port - ${e.message}', tag: 'LatencyTester');
|
||||
return 65535;
|
||||
|
||||
} on TimeoutException catch (e) {
|
||||
stopwatch.stop();
|
||||
KRLogUtil.kr_w('⏱️ 连接超时: $host:$port - $e', tag: 'LatencyTester');
|
||||
return 65535;
|
||||
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('❌ 延迟测试失败: $host:$port - $e', tag: 'KRLatencyTester');
|
||||
return 65535; // 测试失败返回最大值
|
||||
stopwatch.stop();
|
||||
KRLogUtil.kr_e('❌ 测试异常: $host:$port - $e', tag: 'LatencyTester');
|
||||
return 65535;
|
||||
|
||||
} finally {
|
||||
// 确保关闭连接
|
||||
try {
|
||||
await socket?.close();
|
||||
} catch (e) {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量测试多个节点的延迟
|
||||
///
|
||||
/// 参数:
|
||||
/// - nodes: 节点列表,格式为 [{"host": "example.com", "port": 443}]
|
||||
/// - concurrency: 并发数量
|
||||
/// - timeout: 超时时间(毫秒)
|
||||
///
|
||||
/// 返回:
|
||||
/// - 测试结果映射,键为 "host:port",值为延迟时间
|
||||
/// 批量测试多个节点延迟(并发测试)
|
||||
/// 返回 Map<节点tag, 延迟ms>
|
||||
static Future<Map<String, int>> testMultipleNodes({
|
||||
required List<MapEntry<String, SocketAddress>> nodes,
|
||||
int concurrency = 10,
|
||||
int timeout = 5000,
|
||||
int concurrency = 10, // 并发数
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
final results = <String, int>{};
|
||||
final semaphore = Completer<void>();
|
||||
var activeCount = 0;
|
||||
var completedCount = 0;
|
||||
final List<List<MapEntry<String, SocketAddress>>> batches = [];
|
||||
|
||||
KRLogUtil.kr_i('🚀 开始批量延迟测试,共 ${nodes.length} 个节点,并发数: $concurrency', tag: 'KRLatencyTester');
|
||||
// 分批处理
|
||||
for (int i = 0; i < nodes.length; i += concurrency) {
|
||||
batches.add(
|
||||
nodes.sublist(i, i + concurrency > nodes.length ? nodes.length : i + concurrency)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> processNode(MapEntry<String, SocketAddress> node) async {
|
||||
try {
|
||||
final host = node.value.address;
|
||||
final port = node.value.port;
|
||||
final key = node.key;
|
||||
|
||||
final latency = await testNode(
|
||||
host: host,
|
||||
port: port,
|
||||
KRLogUtil.kr_i('📊 开始批量测试: ${nodes.length} 个节点,分 ${batches.length} 批,每批 $concurrency 个', tag: 'LatencyTester');
|
||||
|
||||
int completedCount = 0;
|
||||
|
||||
// 逐批测试
|
||||
for (int batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
||||
final batch = batches[batchIndex];
|
||||
|
||||
KRLogUtil.kr_i('📦 测试第 ${batchIndex + 1}/${batches.length} 批(${batch.length} 个节点)', tag: 'LatencyTester');
|
||||
|
||||
// 并发测试当前批次
|
||||
final futures = batch.map((node) async {
|
||||
final tag = node.key;
|
||||
final address = node.value;
|
||||
|
||||
final latency = await testTcpLatency(
|
||||
host: address.host,
|
||||
port: address.port,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
results[key] = latency;
|
||||
} catch (e) {
|
||||
results[node.key] = 65535;
|
||||
KRLogUtil.kr_e('❌ 节点测试异常: ${node.key} - $e', tag: 'KRLatencyTester');
|
||||
} finally {
|
||||
|
||||
completedCount++;
|
||||
activeCount--;
|
||||
|
||||
if (completedCount >= nodes.length) {
|
||||
semaphore.complete();
|
||||
|
||||
if (completedCount % 5 == 0 || completedCount == nodes.length) {
|
||||
KRLogUtil.kr_i('📈 测试进度: $completedCount/${nodes.length}', tag: 'LatencyTester');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分批处理节点
|
||||
for (var i = 0; i < nodes.length; i += concurrency) {
|
||||
final batch = nodes.skip(i).take(concurrency);
|
||||
|
||||
for (final node in batch) {
|
||||
activeCount++;
|
||||
processNode(node);
|
||||
}
|
||||
|
||||
return MapEntry(tag, latency);
|
||||
}).toList();
|
||||
|
||||
// 等待当前批次完成
|
||||
if (i + concurrency < nodes.length) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
final batchResults = await Future.wait(futures);
|
||||
|
||||
// 收集结果
|
||||
for (final result in batchResults) {
|
||||
results[result.key] = result.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计结果
|
||||
final successCount = results.values.where((latency) => latency < 65535).length;
|
||||
final failCount = results.length - successCount;
|
||||
|
||||
KRLogUtil.kr_i('✅ 批量测试完成', tag: 'LatencyTester');
|
||||
KRLogUtil.kr_i('📊 成功: $successCount, 失败: $failCount', tag: 'LatencyTester');
|
||||
|
||||
// 显示延迟最低的前3个
|
||||
final successNodes = results.entries
|
||||
.where((e) => e.value < 65535)
|
||||
.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
|
||||
if (successNodes.isNotEmpty) {
|
||||
KRLogUtil.kr_i('🏆 延迟最低的前3个节点:', tag: 'LatencyTester');
|
||||
for (int i = 0; i < 3 && i < successNodes.length; i++) {
|
||||
KRLogUtil.kr_i(' ${i + 1}. ${successNodes[i].key}: ${successNodes[i].value}ms', tag: 'LatencyTester');
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
await semaphore.future;
|
||||
|
||||
KRLogUtil.kr_i('✅ 批量延迟测试完成,成功: ${results.length} 个', tag: 'KRLatencyTester');
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/// Socket 地址类
|
||||
/// 表示网络地址和端口的组合
|
||||
/// 节点地址信息
|
||||
class SocketAddress {
|
||||
final String address;
|
||||
final String host;
|
||||
final int port;
|
||||
|
||||
SocketAddress(this.address, this.port);
|
||||
SocketAddress(this.host, this.port);
|
||||
|
||||
@override
|
||||
String toString() => '$address:$port';
|
||||
}
|
||||
String toString() => '$host:$port';
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class KRSecureStorage {
|
||||
|
||||
// 存储箱名称
|
||||
static const String _boxName = 'kaer_secure_storage';
|
||||
|
||||
|
||||
// 加密密钥
|
||||
static const String _encryptionKey = 'kaer_secure_storage_key';
|
||||
|
||||
@@ -27,19 +27,19 @@ class KRSecureStorage {
|
||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
final baseDir = await getApplicationSupportDirectory();
|
||||
KRLogUtil.kr_i('初始化 Hive,路径: ${baseDir.path}', tag: 'SecureStorage');
|
||||
|
||||
|
||||
// 确保目录存在
|
||||
if (!baseDir.existsSync()) {
|
||||
await baseDir.create(recursive: true);
|
||||
KRLogUtil.kr_i('已创建 Hive 目录: ${baseDir.path}', tag: 'SecureStorage');
|
||||
}
|
||||
|
||||
|
||||
await Hive.initFlutter(baseDir.path);
|
||||
} else {
|
||||
// Android 和 iOS 使用默认路径
|
||||
await Hive.initFlutter();
|
||||
}
|
||||
|
||||
|
||||
// 使用加密适配器
|
||||
final key = HiveAesCipher(_generateKey());
|
||||
await Hive.openBox(_boxName, encryptionCipher: key);
|
||||
@@ -48,14 +48,14 @@ class KRSecureStorage {
|
||||
KRLogUtil.kr_e('初始化 Hive 失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('错误类型: ${e.runtimeType}', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('错误堆栈: $stackTrace', tag: 'SecureStorage');
|
||||
|
||||
|
||||
// 对于 Windows 和 Linux,如果初始化失败,尝试删除旧文件并重试
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
try {
|
||||
KRLogUtil.kr_i('尝试清理并重新初始化 Hive', tag: 'SecureStorage');
|
||||
final baseDir = await getApplicationSupportDirectory();
|
||||
final hiveDir = Directory(baseDir.path);
|
||||
|
||||
|
||||
// 查找并删除相关的 Hive 文件
|
||||
if (hiveDir.existsSync()) {
|
||||
final files = hiveDir.listSync();
|
||||
@@ -63,8 +63,8 @@ class KRSecureStorage {
|
||||
if (entity is File) {
|
||||
final fileName = entity.path.split(Platform.pathSeparator).last;
|
||||
// 删除 Hive 数据库文件(通常是 .hive 或 .lock 文件)
|
||||
if (fileName.startsWith(_boxName) ||
|
||||
fileName.endsWith('.hive') ||
|
||||
if (fileName.startsWith(_boxName) ||
|
||||
fileName.endsWith('.hive') ||
|
||||
fileName.endsWith('.lock')) {
|
||||
try {
|
||||
await entity.delete();
|
||||
@@ -76,7 +76,7 @@ class KRSecureStorage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 重新初始化
|
||||
await Hive.initFlutter(baseDir.path);
|
||||
final key = HiveAesCipher(_generateKey());
|
||||
@@ -116,8 +116,15 @@ class KRSecureStorage {
|
||||
try {
|
||||
final box = await _ensureBoxOpen();
|
||||
await box.put(key, value);
|
||||
|
||||
// 🔧 关键修复:强制将数据立即写入磁盘,确保持久化
|
||||
// 不调用 flush() 时,数据只在内存缓冲区中,应用关闭后会丢失!
|
||||
await box.flush();
|
||||
|
||||
print('💾 [SecureStorage] 数据已保存并刷新到磁盘: $key');
|
||||
KRLogUtil.kr_i('✅ 数据已保存: $key', tag: 'SecureStorage');
|
||||
} catch (e) {
|
||||
print('❌ [SecureStorage] 存储数据失败: $e');
|
||||
KRLogUtil.kr_e('❌ 存储数据失败: $e', tag: 'SecureStorage');
|
||||
rethrow; // 重新抛出异常,让调用者知道保存失败
|
||||
}
|
||||
@@ -210,4 +217,4 @@ class KRSecureStorage {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
|
||||
const WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(800, 668),
|
||||
minimumSize: Size(800, 668),
|
||||
minimumSize: Size(400, 334),
|
||||
center: true,
|
||||
backgroundColor: Colors.white,
|
||||
skipTaskbar: false,
|
||||
@@ -47,15 +47,15 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
await windowManager.setTitle('HiFastVPN');
|
||||
await windowManager.setSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(400, 334));
|
||||
await windowManager.center();
|
||||
await windowManager.show();
|
||||
// 阻止窗口关闭
|
||||
await windowManager.setPreventClose(true);
|
||||
} else {
|
||||
await windowManager.setTitle('Kaer VPN');
|
||||
await windowManager.setTitle('HiFastVPN');
|
||||
await windowManager.setSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(400, 334));
|
||||
await windowManager.center();
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
/// 初始化平台通道
|
||||
void _initPlatformChannel() {
|
||||
if (Platform.isMacOS) {
|
||||
const platform = MethodChannel('kaer_vpn/terminate');
|
||||
const platform = MethodChannel('hifast_vpn/terminate');
|
||||
platform.setMethodCallHandler((call) async {
|
||||
if (call.method == 'onTerminate') {
|
||||
KRLogUtil.kr_i('收到应用终止通知');
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
import 'dart:io';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
/// Windows DNS 管理工具类
|
||||
///
|
||||
/// 用于在 Windows 平台上管理系统 DNS 设置
|
||||
/// 主要功能:
|
||||
/// 1. 备份原始 DNS 设置
|
||||
/// 2. 恢复 DNS 设置
|
||||
/// 3. 兜底设置为国内公共 DNS (223.5.5.5 和 114.114.114.114)
|
||||
class KRWindowsDnsUtil {
|
||||
/// 私有构造函数
|
||||
KRWindowsDnsUtil._();
|
||||
|
||||
/// 单例实例
|
||||
static final KRWindowsDnsUtil _instance = KRWindowsDnsUtil._();
|
||||
|
||||
/// 工厂构造函数
|
||||
factory KRWindowsDnsUtil() => _instance;
|
||||
|
||||
/// 获取实例的静态方法
|
||||
static KRWindowsDnsUtil get instance => _instance;
|
||||
|
||||
/// 原始 DNS 服务器地址(连接前备份)
|
||||
List<String>? _originalDnsServers;
|
||||
|
||||
/// 主网络接口名称
|
||||
String? _primaryInterfaceName;
|
||||
|
||||
/// 备份当前 DNS 设置
|
||||
///
|
||||
/// 在连接 VPN 之前调用,保存原始 DNS 配置
|
||||
/// 返回:true-成功,false-失败
|
||||
Future<bool> kr_backupDnsSettings() async {
|
||||
if (!Platform.isWindows) {
|
||||
KRLogUtil.kr_w('❌ 非 Windows 平台,跳过 DNS 备份', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 获取主网络接口
|
||||
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (interfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
_primaryInterfaceName = interfaceName;
|
||||
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||
|
||||
// 2. 获取当前 DNS 服务器
|
||||
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
||||
if (dnsServers.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS');
|
||||
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
||||
} else {
|
||||
_originalDnsServers = dnsServers;
|
||||
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复原始 DNS 设置
|
||||
///
|
||||
/// 在断开 VPN 后调用,恢复备份的 DNS 配置
|
||||
/// 如果恢复失败,会自动调用兜底机制设置为公共DNS
|
||||
/// 返回:true-成功,false-失败
|
||||
Future<bool> kr_restoreDnsSettings() async {
|
||||
if (!Platform.isWindows) {
|
||||
KRLogUtil.kr_w('❌ 非 Windows 平台,跳过 DNS 恢复', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 检查是否有备份
|
||||
if (_primaryInterfaceName == null) {
|
||||
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS');
|
||||
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (_primaryInterfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 恢复原始 DNS
|
||||
if (_originalDnsServers == null) {
|
||||
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
if (_originalDnsServers!.isEmpty) {
|
||||
// 原本是 DHCP 自动获取
|
||||
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
|
||||
final success = await _kr_setDnsToAuto(_primaryInterfaceName!);
|
||||
if (!success) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复 DHCP 失败,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
} else {
|
||||
// 恢复指定的 DNS 服务器
|
||||
KRLogUtil.kr_i('🔄 恢复原始 DNS: ${_originalDnsServers!.join(", ")}', tag: 'WindowsDNS');
|
||||
final success = await _kr_setDnsServers(
|
||||
_primaryInterfaceName!,
|
||||
_originalDnsServers!,
|
||||
);
|
||||
if (!success) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复原始 DNS 失败,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 验证 DNS 是否恢复成功
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
final currentDns = await _kr_getCurrentDnsServers(_primaryInterfaceName!);
|
||||
KRLogUtil.kr_i('✅ 当前 DNS: ${currentDns.join(", ")}', tag: 'WindowsDNS');
|
||||
|
||||
// 4. 额外验证:确认 DNS 不再指向 127.0.0.1(sing-box 的本地 DNS)
|
||||
final hasLocalhost = currentDns.any((dns) => dns.startsWith('127.'));
|
||||
if (hasLocalhost) {
|
||||
KRLogUtil.kr_w('⚠️ DNS 仍包含 127.0.0.1,可能未完全恢复', tag: 'WindowsDNS');
|
||||
KRLogUtil.kr_w('⚠️ 执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||
KRLogUtil.kr_w('⚠️ 执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
}
|
||||
|
||||
/// 兜底恢复:强制设置为国内公共 DNS
|
||||
///
|
||||
/// 当正常恢复失败时,设置为安全的公共 DNS 服务器
|
||||
/// - 主 DNS: 223.5.5.5 (阿里云)
|
||||
/// - 备用 DNS: 114.114.114.114 (114DNS)
|
||||
Future<bool> _kr_fallbackRestoreDns() async {
|
||||
try {
|
||||
KRLogUtil.kr_w('🆘 执行 DNS 兜底恢复机制', tag: 'WindowsDNS');
|
||||
KRLogUtil.kr_i('🔧 设置为国内公共 DNS: 223.5.5.5, 114.114.114.114', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 获取主网络接口(如果还没有)
|
||||
if (_primaryInterfaceName == null) {
|
||||
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (_primaryInterfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法检测网络接口,兜底恢复失败', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 设置为公共 DNS
|
||||
final fallbackDns = ['223.5.5.5', '114.114.114.114'];
|
||||
final success = await _kr_setDnsServers(_primaryInterfaceName!, fallbackDns);
|
||||
|
||||
if (success) {
|
||||
KRLogUtil.kr_i('✅ 兜底 DNS 设置成功', tag: 'WindowsDNS');
|
||||
|
||||
// 3. 验证设置
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
final currentDns = await _kr_getCurrentDnsServers(_primaryInterfaceName!);
|
||||
KRLogUtil.kr_i('✅ 验证当前 DNS: ${currentDns.join(", ")}', tag: 'WindowsDNS');
|
||||
|
||||
return true;
|
||||
} else {
|
||||
KRLogUtil.kr_e('❌ 兜底 DNS 设置失败', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 兜底恢复失败: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取主网络接口名称
|
||||
///
|
||||
/// 通过 netsh 命令查找处于"已连接"状态的主网络接口
|
||||
/// 返回:接口名称,失败返回 null
|
||||
Future<String?> _kr_getPrimaryNetworkInterface() async {
|
||||
try {
|
||||
// 使用 netsh 获取接口列表
|
||||
final result = await Process.run('netsh', ['interface', 'show', 'interface']);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
return null;
|
||||
}
|
||||
|
||||
final output = result.stdout.toString();
|
||||
final lines = output.split('\n');
|
||||
|
||||
// 收集所有已连接的接口
|
||||
final connectedInterfaces = <String>[];
|
||||
|
||||
// 查找所有"已连接"的接口
|
||||
// 支持中文和英文 Windows 系统
|
||||
for (var line in lines) {
|
||||
// 中文: "已连接", 英文: "Connected", "Enabled"
|
||||
if (line.contains('已连接') ||
|
||||
line.contains('Connected') ||
|
||||
line.toLowerCase().contains('enabled')) {
|
||||
// 跳过表头行
|
||||
if (line.contains('Admin') ||
|
||||
line.contains('管理') ||
|
||||
line.contains('State') ||
|
||||
line.contains('状态')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析接口名称(最后一列)
|
||||
final parts = line.trim().split(RegExp(r'\s{2,}'));
|
||||
if (parts.length >= 4) {
|
||||
final interfaceName = parts.last.trim();
|
||||
// 排除空接口名
|
||||
if (interfaceName.isNotEmpty && interfaceName.length > 1) {
|
||||
connectedInterfaces.add(interfaceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connectedInterfaces.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 未找到已连接的网络接口', tag: 'WindowsDNS');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 🔧 优化:优先选择有线网络(以太网),然后才是 Wi-Fi
|
||||
// 有线网络通常更稳定
|
||||
String? selectedInterface;
|
||||
for (var interface in connectedInterfaces) {
|
||||
final lowerName = interface.toLowerCase();
|
||||
// 优先选择以太网
|
||||
if (lowerName.contains('ethernet') ||
|
||||
lowerName.contains('以太网') ||
|
||||
lowerName.contains('lan') ||
|
||||
lowerName.contains('local')) {
|
||||
selectedInterface = interface;
|
||||
KRLogUtil.kr_i('🔍 选择有线网络接口: $interface', tag: 'WindowsDNS');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有有线网络,选择第一个(通常是 Wi-Fi)
|
||||
selectedInterface ??= connectedInterfaces.first;
|
||||
if (selectedInterface != connectedInterfaces.first) {
|
||||
KRLogUtil.kr_d('🔍 选择网络接口: $selectedInterface', tag: 'WindowsDNS');
|
||||
} else {
|
||||
KRLogUtil.kr_i('🔍 选择网络接口: $selectedInterface', tag: 'WindowsDNS');
|
||||
}
|
||||
|
||||
return selectedInterface;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 获取网络接口异常: $e', tag: 'WindowsDNS');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定接口的当前 DNS 服务器
|
||||
///
|
||||
/// 参数:
|
||||
/// - interfaceName: 网络接口名称
|
||||
///
|
||||
/// 返回:DNS 服务器列表
|
||||
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
|
||||
try {
|
||||
final result = await Process.run('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'show',
|
||||
'dnsservers',
|
||||
'name="$interfaceName"',
|
||||
]);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
KRLogUtil.kr_e('❌ 获取 DNS 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
return [];
|
||||
}
|
||||
|
||||
final output = result.stdout.toString();
|
||||
final dnsServers = <String>[];
|
||||
|
||||
// 解析 DNS 服务器地址
|
||||
final lines = output.split('\n');
|
||||
for (var line in lines) {
|
||||
// 查找 IP 地址格式的行
|
||||
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
|
||||
if (ipMatch != null) {
|
||||
final ip = ipMatch.group(0)!;
|
||||
// 排除本地回环地址
|
||||
if (!ip.startsWith('127.')) {
|
||||
dnsServers.add(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_d('🔍 当前 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||
return dnsServers;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 获取 DNS 异常: $e', tag: 'WindowsDNS');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置指定接口的 DNS 服务器
|
||||
///
|
||||
/// 参数:
|
||||
/// - interfaceName: 网络接口名称
|
||||
/// - dnsServers: DNS 服务器列表
|
||||
///
|
||||
/// 返回:true-成功,false-失败
|
||||
Future<bool> _kr_setDnsServers(String interfaceName, List<String> dnsServers) async {
|
||||
try {
|
||||
if (dnsServers.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ DNS 列表为空,无法设置', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 设置主 DNS
|
||||
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
|
||||
var result = await Process.run('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'set',
|
||||
'dnsservers',
|
||||
'name="$interfaceName"',
|
||||
'source=static',
|
||||
'address=${dnsServers[0]}',
|
||||
'validate=no',
|
||||
]);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
KRLogUtil.kr_e('❌ 设置主 DNS 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 设置备用 DNS(如果有)
|
||||
if (dnsServers.length > 1) {
|
||||
for (int i = 1; i < dnsServers.length; i++) {
|
||||
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
|
||||
result = await Process.run('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'add',
|
||||
'dnsservers',
|
||||
'name="$interfaceName"',
|
||||
'address=${dnsServers[i]}',
|
||||
'index=${i + 1}',
|
||||
'validate=no',
|
||||
]);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
KRLogUtil.kr_w('⚠️ 设置备用 DNS $i 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
// 继续设置下一个,不中断
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 刷新 DNS 缓存
|
||||
await _kr_flushDnsCache();
|
||||
|
||||
KRLogUtil.kr_i('✅ DNS 服务器设置完成', tag: 'WindowsDNS');
|
||||
return true;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 设置 DNS 异常: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 DNS 为自动获取(DHCP)
|
||||
///
|
||||
/// 参数:
|
||||
/// - interfaceName: 网络接口名称
|
||||
///
|
||||
/// 返回:true-成功,false-失败
|
||||
Future<bool> _kr_setDnsToAuto(String interfaceName) async {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
|
||||
|
||||
final result = await Process.run('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'set',
|
||||
'dnsservers',
|
||||
'name="$interfaceName"',
|
||||
'source=dhcp',
|
||||
]);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
KRLogUtil.kr_e('❌ 设置 DHCP 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 刷新 DNS 缓存
|
||||
await _kr_flushDnsCache();
|
||||
|
||||
KRLogUtil.kr_i('✅ DNS 已设置为自动获取', tag: 'WindowsDNS');
|
||||
return true;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 设置 DHCP 异常: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新 DNS 缓存
|
||||
///
|
||||
/// 执行 ipconfig /flushdns 命令清空 DNS 解析缓存
|
||||
Future<void> _kr_flushDnsCache() async {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
|
||||
|
||||
final result = await Process.run('ipconfig', ['/flushdns']);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
|
||||
} else {
|
||||
KRLogUtil.kr_w('⚠️ 刷新 DNS 缓存失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 刷新 DNS 缓存异常: $e', tag: 'WindowsDNS');
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除备份数据
|
||||
///
|
||||
/// 在应用退出或不需要时调用
|
||||
void kr_clearBackup() {
|
||||
_originalDnsServers = null;
|
||||
_primaryInterfaceName = null;
|
||||
KRLogUtil.kr_d('🗑️ 已清除 DNS 备份数据', tag: 'WindowsDNS');
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ class HIBaseScaffold extends StatelessWidget {
|
||||
/// 是否显示背景图
|
||||
final bool showBackgroundImage;
|
||||
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// resizeToAvoidBottomInset
|
||||
final bool? resizeToAvoidBottomInset;
|
||||
|
||||
@@ -59,6 +61,7 @@ class HIBaseScaffold extends StatelessWidget {
|
||||
this.showBackgroundImage = true,
|
||||
this.topContentAreaHeight = 120.0,
|
||||
this.resizeToAvoidBottomInset = false,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -71,7 +74,7 @@ class HIBaseScaffold extends StatelessWidget {
|
||||
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
|
||||
backgroundColor: showBackgroundImage
|
||||
? Colors.transparent
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
: (backgroundColor ?? Theme.of(context).scaffoldBackgroundColor),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
toolbarHeight: 0.0,
|
||||
@@ -152,7 +155,7 @@ class HIBaseScaffold extends StatelessWidget {
|
||||
if (title != null || subtitle != null)
|
||||
Positioned(
|
||||
top: topPadding +
|
||||
(showBack ? 12.w : 20.w), // 顶部空出状态栏高度,并根据是否有返回按钮调整间距
|
||||
(showBack ? 8 : 20), // 顶部空出状态栏高度,并根据是否有返回按钮调整间距
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
@@ -164,7 +167,7 @@ class HIBaseScaffold extends StatelessWidget {
|
||||
title!,
|
||||
style: TextStyle(
|
||||
color: Colors.black, // 在深色背景下使用白色
|
||||
fontSize: 24.sp,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -175,7 +178,7 @@ class HIBaseScaffold extends StatelessWidget {
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
color: Colors.black, // 副标题使用带透明度的白色
|
||||
fontSize: 14.sp,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w100,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'swipe/swipe_controller.dart';
|
||||
import 'swipe/swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
class HIEdgeSwipeDetector extends StatefulWidget {
|
||||
final Widget child;
|
||||
final double reservedEdgeWidth;
|
||||
final double detectionEdgeWidth;
|
||||
final double minSwipeDistance;
|
||||
final double minSwipeVelocity;
|
||||
final SwipeConfig? config;
|
||||
const HIEdgeSwipeDetector(
|
||||
{super.key,
|
||||
required this.child,
|
||||
this.reservedEdgeWidth = 0,
|
||||
this.detectionEdgeWidth = 20,
|
||||
this.minSwipeDistance = 20,
|
||||
this.minSwipeVelocity = 300,
|
||||
this.config});
|
||||
@override
|
||||
State<HIEdgeSwipeDetector> createState() => _HIEdgeSwipeDetectorState();
|
||||
}
|
||||
|
||||
class _HIEdgeSwipeDetectorState extends State<HIEdgeSwipeDetector> {
|
||||
Offset? _start;
|
||||
Offset? _last;
|
||||
DateTime? _startTime;
|
||||
final SwipeController _controller =
|
||||
Get.put(SwipeController(), permanent: true);
|
||||
bool _fromLeft = false;
|
||||
bool _fromRight = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
KRLogUtil.kr_d('HIEdgeSwipeDetector build');
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
widget.child,
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: widget.detectionEdgeWidth,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragStart: (details) {
|
||||
_fromLeft = true;
|
||||
_fromRight = false;
|
||||
_start = details.globalPosition;
|
||||
_startTime = DateTime.now();
|
||||
KRLogUtil.kr_d('dragStart LEFT x=${details.globalPosition.dx}',
|
||||
tag: 'HIEdgeSwipeDetector');
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
_last = details.globalPosition;
|
||||
KRLogUtil.kr_d('dragUpdate LEFT dx=${details.delta.dx}',
|
||||
tag: 'HIEdgeSwipeDetector');
|
||||
},
|
||||
onHorizontalDragEnd: (details) {
|
||||
if (_start == null || _startTime == null) return;
|
||||
final durationMs =
|
||||
DateTime.now().difference(_startTime!).inMilliseconds;
|
||||
final velocity = details.velocity.pixelsPerSecond.dx.abs();
|
||||
final distance = _last != null ? (_last!.dx - _start!.dx) : 0.0;
|
||||
final okByVelocity = velocity >= widget.minSwipeVelocity;
|
||||
final okByDistance = distance.abs() >= widget.minSwipeDistance;
|
||||
KRLogUtil.kr_d(
|
||||
'dragEnd LEFT vel=${details.velocity.pixelsPerSecond.dx} dur=$durationMs dist=$distance okV=$okByVelocity okD=$okByDistance',
|
||||
tag: 'HIEdgeSwipeDetector');
|
||||
if (!okByVelocity && !okByDistance) {
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
if (distance > 0) {
|
||||
final cfg = widget.config;
|
||||
if (cfg == null || !cfg.enableLeft) {
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
if (cfg.onLeft != null) {
|
||||
cfg.onLeft!();
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
_reset();
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: widget.detectionEdgeWidth,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragStart: (details) {
|
||||
_fromLeft = false;
|
||||
_fromRight = true;
|
||||
_start = details.globalPosition;
|
||||
_startTime = DateTime.now();
|
||||
KRLogUtil.kr_d('dragStart RIGHT x=${details.globalPosition.dx}',
|
||||
tag: 'HIEdgeSwipeDetector');
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
_last = details.globalPosition;
|
||||
KRLogUtil.kr_d('dragUpdate RIGHT dx=${details.delta.dx}',
|
||||
tag: 'HIEdgeSwipeDetector');
|
||||
},
|
||||
onHorizontalDragEnd: (details) {
|
||||
if (_start == null || _startTime == null) return;
|
||||
final durationMs =
|
||||
DateTime.now().difference(_startTime!).inMilliseconds;
|
||||
final velocity = details.velocity.pixelsPerSecond.dx.abs();
|
||||
final distance = _last != null ? (_last!.dx - _start!.dx) : 0.0;
|
||||
final okByVelocity = velocity >= widget.minSwipeVelocity;
|
||||
final okByDistance = distance.abs() >= widget.minSwipeDistance;
|
||||
KRLogUtil.kr_d(
|
||||
'dragEnd RIGHT vel=${details.velocity.pixelsPerSecond.dx} dur=$durationMs dist=$distance okV=$okByVelocity okD=$okByDistance',
|
||||
tag: 'HIEdgeSwipeDetector');
|
||||
if (!okByVelocity && !okByDistance) {
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
if (distance < 0) {
|
||||
final cfg = widget.config;
|
||||
if (cfg == null || !cfg.enableRight) {
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
if (cfg.onRight != null) {
|
||||
cfg.onRight!();
|
||||
} else {
|
||||
_controller.openRightMenu();
|
||||
}
|
||||
}
|
||||
_reset();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_start = null;
|
||||
_last = null;
|
||||
_startTime = null;
|
||||
_fromLeft = false;
|
||||
_fromRight = false;
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,16 @@ class HiFixedScrollbar extends StatefulWidget {
|
||||
|
||||
/// 距离右边的间距(默认 18)
|
||||
final double right;
|
||||
|
||||
/// 滚动条宽度
|
||||
final double thickness;
|
||||
|
||||
/// 滚动条颜色(默认白色 30%)
|
||||
final Color thumbColor;
|
||||
|
||||
/// 背景轨道颜色(默认白色 15%)
|
||||
final Color trackColor;
|
||||
|
||||
/// 滚动条固定高度(默认 50)
|
||||
final double thumbHeight;
|
||||
|
||||
@@ -36,6 +40,7 @@ class HiFixedScrollbar extends StatefulWidget {
|
||||
|
||||
class _HiFixedScrollbarState extends State<HiFixedScrollbar> {
|
||||
double _thumbOffset = 0.0;
|
||||
bool _hasScrolled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -58,6 +63,7 @@ class _HiFixedScrollbarState extends State<HiFixedScrollbar> {
|
||||
|
||||
setState(() {
|
||||
_thumbOffset = (trackHeight * scrollRatio).clamp(0, trackHeight);
|
||||
_hasScrolled = offset > 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,41 +77,44 @@ class _HiFixedScrollbarState extends State<HiFixedScrollbar> {
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
final showScrollbar = widget.isShowScrollbar &&
|
||||
widget.controller.hasClients &&
|
||||
widget.controller.position.maxScrollExtent > 0 &&
|
||||
_hasScrolled;
|
||||
return Stack(
|
||||
children: [
|
||||
// 滚动内容
|
||||
widget.child,
|
||||
|
||||
if(widget.isShowScrollbar)
|
||||
...[
|
||||
// 滚动条轨道
|
||||
Positioned(
|
||||
right: widget.right.w,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: widget.thickness.w,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.trackColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
if (showScrollbar) ...[
|
||||
// 滚动条轨道
|
||||
Positioned(
|
||||
right: widget.right.w,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: widget.thickness.w,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.trackColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 滚动条拇指
|
||||
Positioned(
|
||||
right: widget.right.w,
|
||||
top: _thumbOffset,
|
||||
child: Container(
|
||||
width: widget.thickness.w,
|
||||
height: widget.thumbHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.thumbColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
// 滚动条拇指
|
||||
Positioned(
|
||||
right: widget.right.w,
|
||||
top: _thumbOffset,
|
||||
child: Container(
|
||||
width: widget.thickness.w,
|
||||
height: widget.thumbHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.thumbColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class KRSubscriptionExpiryText extends StatelessWidget {
|
||||
final String? Function() expireTimeProvider;
|
||||
final TextStyle? style;
|
||||
|
||||
const KRSubscriptionExpiryText({
|
||||
Key? key,
|
||||
required this.expireTimeProvider,
|
||||
this.style,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final expireTimeStr = expireTimeProvider();
|
||||
String expiryText;
|
||||
bool highlight = false;
|
||||
|
||||
if (expireTimeStr == null || expireTimeStr.isEmpty) {
|
||||
expiryText = '尚未购买套餐';
|
||||
highlight = true;
|
||||
} else {
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime = DateTime.parse(expireTimeStr);
|
||||
} catch (_) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(DateTime.now())) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
highlight = true;
|
||||
} else {
|
||||
final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
|
||||
final baseStyle = style ??
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
);
|
||||
final appliedStyle = highlight
|
||||
? baseStyle.copyWith(color: const Color(0xFFFF00B7))
|
||||
: baseStyle;
|
||||
|
||||
return Text(
|
||||
expiryText,
|
||||
style: appliedStyle,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'swipe_config.dart';
|
||||
|
||||
abstract class HasSwipeConfig {
|
||||
SwipeConfig get swipeConfig;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SwipeConfig {
|
||||
final bool enableLeft;
|
||||
final bool enableRight;
|
||||
final VoidCallback? onLeft;
|
||||
final VoidCallback? onRight;
|
||||
const SwipeConfig({this.enableLeft = false, this.enableRight = false, this.onLeft, this.onRight});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
import 'swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
class SwipeController extends GetxController {
|
||||
final Rxn<SwipeConfig> currentConfig = Rxn<SwipeConfig>();
|
||||
final RxInt currentPageIndex = 0.obs;
|
||||
final RxBool leftMenuOpen = false.obs;
|
||||
final RxBool rightMenuOpen = false.obs;
|
||||
void setConfig(SwipeConfig? config) {
|
||||
currentConfig.value = config;
|
||||
}
|
||||
|
||||
void setPageIndex(int index) {
|
||||
currentPageIndex.value = index;
|
||||
}
|
||||
|
||||
void closeMenus() {
|
||||
leftMenuOpen.value = false;
|
||||
rightMenuOpen.value = false;
|
||||
}
|
||||
|
||||
void openLeftMenu() {
|
||||
KRLogUtil.kr_d('openLeftMenu', tag: 'SwipeController');
|
||||
leftMenuOpen.value = true;
|
||||
Get.toNamed(Routes.HI_MENU);
|
||||
}
|
||||
|
||||
void openRightMenu() {
|
||||
KRLogUtil.kr_d('openRightMenu', tag: 'SwipeController');
|
||||
rightMenuOpen.value = true;
|
||||
Get.toNamed(Routes.KR_SETTING);
|
||||
}
|
||||
|
||||
void triggerLeft() {
|
||||
final cfg = currentConfig.value;
|
||||
if (cfg == null || !cfg.enableLeft) return;
|
||||
KRLogUtil.kr_d('call11 onLeft$cfg', tag: 'SwipeController');
|
||||
|
||||
if (cfg.onLeft != null) {
|
||||
KRLogUtil.kr_d('call onLeft', tag: 'SwipeController');
|
||||
cfg.onLeft!.call();
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void triggerRight() {
|
||||
final cfg = currentConfig.value;
|
||||
if (cfg == null || !cfg.enableRight) return;
|
||||
if (cfg.onRight != null) {
|
||||
KRLogUtil.kr_d('call onRight', tag: 'SwipeController');
|
||||
cfg.onRight!.call();
|
||||
return;
|
||||
}
|
||||
openRightMenu();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../hi_edge_swipe_detector.dart';
|
||||
import 'swipe_config.dart';
|
||||
import 'has_swipe_config.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
class SwipeWrapper extends StatelessWidget {
|
||||
final Widget child;
|
||||
final SwipeConfig config;
|
||||
const SwipeWrapper({super.key, required this.child, required this.config});
|
||||
|
||||
static Widget detect(Widget Function() builder) {
|
||||
final w = builder();
|
||||
SwipeConfig effective = const SwipeConfig();
|
||||
if (w is HasSwipeConfig) {
|
||||
effective = (w as HasSwipeConfig).swipeConfig;
|
||||
} else if (GetPlatform.isIOS) {
|
||||
effective = SwipeConfig(enableLeft: true, onLeft: () => Get.back());
|
||||
}
|
||||
final enabled = effective.enableLeft ||
|
||||
effective.enableRight ||
|
||||
effective.onLeft != null ||
|
||||
effective.onRight != null;
|
||||
KRLogUtil.kr_d(
|
||||
'SwipeWrapper.detect enabled=$enabled left=${effective.enableLeft} right=${effective.enableRight}',
|
||||
tag: 'SwipeWrapper');
|
||||
if (!enabled) return w;
|
||||
return HIEdgeSwipeDetector(child: w, config: effective);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effective = config;
|
||||
final enabled = effective.enableLeft ||
|
||||
effective.enableRight ||
|
||||
effective.onLeft != null ||
|
||||
effective.onRight != null;
|
||||
KRLogUtil.kr_d(
|
||||
'SwipeWrapper.build enabled=$enabled left=${effective.enableLeft} right=${effective.enableRight}',
|
||||
tag: 'SwipeWrapper');
|
||||
if (!enabled) return child;
|
||||
return HIEdgeSwipeDetector(child: child, config: effective);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // 已替换为自定义组件
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
|
||||
|
||||
@@ -37,6 +38,12 @@ void main() async {
|
||||
// ✅ 等待 Flutter 确保窗口尺寸准备完毕(关键修复)
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown],
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化日志系统
|
||||
KRLogUtil.kr_init();
|
||||
|
||||
@@ -175,6 +182,7 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
|
||||
Routes.HI_MENU,
|
||||
Routes.KR_HOME,
|
||||
Routes.HI_USER_INFO,
|
||||
Routes.KR_ORDER_STATUS,
|
||||
];
|
||||
print('routing.current${routing.current}');
|
||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
create-dmg "HiFastVPN.app" \
|
||||
--overwrite \
|
||||
--dmg-title="HiFastVPN" \
|
||||
--app-drop-link \
|
||||
--dmg "HiFastVPN.dmg"
|
||||
@@ -12,6 +12,7 @@ import flutter_udid
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import screen_retriever_macos
|
||||
import share_plus
|
||||
import tray_manager
|
||||
import url_launcher_macos
|
||||
import webview_flutter_wkwebview
|
||||
@@ -25,6 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
|
||||
@@ -21,6 +21,8 @@ PODS:
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_retriever_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- share_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- tray_manager (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
@@ -40,6 +42,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||
@@ -68,6 +71,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
screen_retriever_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
|
||||
share_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
tray_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
url_launcher_macos:
|
||||
@@ -89,6 +94,7 @@ SPEC CHECKSUMS:
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
|
||||
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
webview_flutter_wkwebview: a4af96a051138e28e29f60101d094683b9f82188
|
||||
|
||||
@@ -571,11 +571,12 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = NJRRF427XB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -583,7 +584,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
@@ -704,11 +705,12 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = NJRRF427XB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -716,7 +718,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -731,11 +733,12 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = NJRRF427XB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -743,7 +746,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ class AppDelegate: FlutterAppDelegate {
|
||||
|
||||
// 通知 Flutter 端应用即将终止
|
||||
if let controller = NSApp.windows.first?.contentViewController as? FlutterViewController {
|
||||
let channel = FlutterMethodChannel(name: "kaer_vpn/terminate", binaryMessenger: controller.engine.binaryMessenger)
|
||||
let channel = FlutterMethodChannel(name: "hifast_vpn/terminate", binaryMessenger: controller.engine.binaryMessenger)
|
||||
channel.invokeMethod("onTerminate", arguments: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
PRODUCT_NAME = HiFastVPN
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.taw.hifastvpn.mac
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 HiFastVPN.com. All rights reserved.
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// 用于发布版本的签名设置
|
||||
|
||||
// 使用 Developer ID Application 证书进行签名(用于 App Store 外分发)
|
||||
CODE_SIGN_IDENTITY = Developer ID Application: Civil Rights Corps (3UR892FAP3)
|
||||
CODE_SIGN_IDENTITY = Developer ID Application: Civil Rights Corps (NJRRF427XB)
|
||||
|
||||
// 开发团队 ID
|
||||
DEVELOPMENT_TEAM = 3UR892FAP3
|
||||
DEVELOPMENT_TEAM = NJRRF427XB
|
||||
|
||||
// 启用代码签名
|
||||
CODE_SIGN_STYLE = Manual
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.networking.networkextension</key>
|
||||
<array/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(TeamIdentifierPrefix)</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
|
||||
@@ -18,22 +18,6 @@
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>clash</string>
|
||||
<string>clashmeta</string>
|
||||
<string>sing-box</string>
|
||||
<string>hiddify</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.networking.networkextension</key>
|
||||
<array/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(TeamIdentifierPrefix)</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
title: Hiddify
|
||||
title: HiFastVPN
|
||||
volume: HiFastVPN
|
||||
contents:
|
||||
- x: 448
|
||||
y: 344
|
||||
@@ -7,4 +8,4 @@ contents:
|
||||
- x: 192
|
||||
y: 344
|
||||
type: file
|
||||
path: Hiddify.app
|
||||
path: HiFastVPN.app
|
||||
|
||||
+26
-2
@@ -254,6 +254,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1049,10 +1057,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "1.0.6"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1413,6 +1421,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.2"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.0.1+1
|
||||
version: 0.0.4+100
|
||||
|
||||
environment:
|
||||
sdk: '>=3.5.0 <4.0.0'
|
||||
@@ -79,6 +79,7 @@ dependencies:
|
||||
flutter_inappwebview: ^6.1.5 # 最新稳定版本
|
||||
crisp_sdk: ^1.1.0 # 使用 crisp_sdk,配合最新的 flutter_inappwebview
|
||||
protocol_handler_windows: ^0.2.0
|
||||
share_plus: ^7.2.2
|
||||
|
||||
# 国际化
|
||||
slang: ^3.30.1
|
||||
@@ -302,4 +303,3 @@ ffigen:
|
||||
headers:
|
||||
entry-points:
|
||||
- "libcore/bin/libcore.h"
|
||||
|
||||
|
||||
@@ -126,6 +126,11 @@ install(FILES "../libcore/bin/HiFastVPNCli.exe"
|
||||
COMPONENT Runtime
|
||||
OPTIONAL)
|
||||
|
||||
install(FILES "runner/resources/app_icon.ico"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime
|
||||
OPTIONAL)
|
||||
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||
|
||||
@@ -7,6 +7,7 @@ echo 正在复制必要的 DLL 文件...
|
||||
copy "%VC_REDIST_DIR%\msvcp140.dll" "%DLL_DIR%"
|
||||
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
||||
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
||||
copy "%~dp0..\libcore\libcore.dll" "%DLL_DIR%"
|
||||
|
||||
echo DLL 文件复制完成!
|
||||
pause
|
||||
pause
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
@@ -29,6 +30,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
TrayManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
permission_handler_windows
|
||||
protocol_handler_windows
|
||||
screen_retriever_windows
|
||||
share_plus
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
|
||||
@@ -14,6 +14,8 @@ Compression=lzma
|
||||
SolidCompression=yes
|
||||
SetupIconFile={{SETUP_ICON_FILE}}
|
||||
WizardStyle=modern
|
||||
LanguageDetectionMethod=uilanguage
|
||||
UsePreviousTasks=no
|
||||
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
@@ -26,6 +28,8 @@ CloseApplications=force
|
||||
{% if locale == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %}
|
||||
{% if locale == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %}
|
||||
{% if locale == 'zh' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
|
||||
{% if locale == 'zh-cn' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
|
||||
{% if locale == 'zh_CN' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
|
||||
{% if locale == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %}
|
||||
{% if locale == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %}
|
||||
{% if locale == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %}
|
||||
@@ -50,7 +54,7 @@ CloseApplications=force
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %}
|
||||
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||
[Files]
|
||||
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
@@ -6,8 +6,11 @@ executable_name: HiFastVPN.exe
|
||||
output_base_file_name: HiFastVPN.exe
|
||||
create_desktop_icon: true
|
||||
install_dir_name: "{autopf64}\\HiFastVPN"
|
||||
setup_icon_file: ..\..\windows\runner\resources\app_icon.ico
|
||||
setup_icon_file: C:\Users\xprotocal\Downloads\hi-client\windows\runner\resources\app_icon.ico
|
||||
locales:
|
||||
- zh
|
||||
- zh-cn
|
||||
- zh_CN
|
||||
- ar
|
||||
- en
|
||||
- fa
|
||||
@@ -15,3 +18,4 @@ locales:
|
||||
- pt
|
||||
- tr
|
||||
script_template: inno_setup.sas
|
||||
languages_dir: compiler:Languages
|
||||
|
||||
Executable → Regular
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 5.3 KiB |
Reference in New Issue
Block a user