Compare commits
51 Commits
dev
..
1b1b38aba5
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -1,3 +1,4 @@
|
|||||||
[submodule "libcore"]
|
[submodule "libcore"]
|
||||||
path = libcore
|
path = libcore
|
||||||
url = https://github.com/hiddify/hiddify-next-core
|
url = https://github.com/hiddify/hiddify-next-core
|
||||||
|
branch = main
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ linux-release:
|
|||||||
flutter_distributor package --flutter-build-args=verbose --platform linux --targets deb,rpm,appimage $(DISTRIBUTOR_ARGS)
|
flutter_distributor package --flutter-build-args=verbose --platform linux --targets deb,rpm,appimage $(DISTRIBUTOR_ARGS)
|
||||||
|
|
||||||
macos-release:
|
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
|
ios-release: #not tested
|
||||||
flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist $(DISTRIBUTOR_ARGS)
|
flutter_distributor package --platform ios --targets ipa --build-export-options-plist ios/exportOptions.plist $(DISTRIBUTOR_ARGS)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.hifastvpn.com"
|
applicationId "app.hifastvpn.com"
|
||||||
minSdkVersion flutter.minSdkVersion
|
minSdkVersion flutter.minSdkVersion
|
||||||
targetSdkVersion 36
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
@@ -105,8 +105,8 @@ android {
|
|||||||
debugSymbolLevel 'FULL'
|
debugSymbolLevel 'FULL'
|
||||||
}
|
}
|
||||||
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
||||||
minifyEnabled true
|
minifyEnabled false
|
||||||
shrinkResources true
|
shrinkResources false
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
// 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.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_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
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:banner="@mipmap/ic_banner"
|
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 |
@@ -193,7 +193,7 @@
|
|||||||
"rewardDetails": "Reward Details >",
|
"rewardDetails": "Reward Details >",
|
||||||
"steps": "Invitation Steps",
|
"steps": "Invitation Steps",
|
||||||
"inviteFriend": "Invite Friends",
|
"inviteFriend": "Invite Friends",
|
||||||
"acceptInvite": "Friends accept invitation\nPlace order and register",
|
"acceptInvite": "Friends accept invitationPlace order and register",
|
||||||
"getReward": "Get Reward",
|
"getReward": "Get Reward",
|
||||||
"shareLink": "Share Link",
|
"shareLink": "Share Link",
|
||||||
"shareQR": "Share QR Code",
|
"shareQR": "Share QR Code",
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
"content": "Would you like to update now?",
|
"content": "Would you like to update now?",
|
||||||
"updateNow": "Update Now",
|
"updateNow": "Update Now",
|
||||||
"updateLater": "Later",
|
"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": {
|
"country": {
|
||||||
"cn": "China",
|
"cn": "China",
|
||||||
|
|||||||
@@ -256,7 +256,7 @@
|
|||||||
"rewardDetails": "Detalles de recompensa >",
|
"rewardDetails": "Detalles de recompensa >",
|
||||||
"steps": "Pasos de invitación",
|
"steps": "Pasos de invitación",
|
||||||
"inviteFriend": "Invitar amigo",
|
"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",
|
"getReward": "Obtener recompensa",
|
||||||
"shareLink": "Compartir por enlace",
|
"shareLink": "Compartir por enlace",
|
||||||
"shareQR": "Compartir por código QR",
|
"shareQR": "Compartir por código QR",
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
"content": "¿Actualizar ahora?",
|
"content": "¿Actualizar ahora?",
|
||||||
"updateNow": "Actualizar ahora",
|
"updateNow": "Actualizar ahora",
|
||||||
"updateLater": "Más tarde",
|
"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": {
|
"kr_invite": {
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
"rewardDetails": "Tasu üksikasjad >",
|
"rewardDetails": "Tasu üksikasjad >",
|
||||||
"steps": "Kutse Sammud",
|
"steps": "Kutse Sammud",
|
||||||
"inviteFriend": "Kutsu Sõbrad",
|
"inviteFriend": "Kutsu Sõbrad",
|
||||||
"acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru",
|
"acceptInvite": "Sõbrad aktsepteerivad kutsetTee tellimus ja registreeru",
|
||||||
"getReward": "Saada Tasu",
|
"getReward": "Saada Tasu",
|
||||||
"shareLink": "Jaga Linki",
|
"shareLink": "Jaga Linki",
|
||||||
"shareQR": "Jaga QR-koodi",
|
"shareQR": "Jaga QR-koodi",
|
||||||
@@ -395,7 +395,7 @@
|
|||||||
"content": "Uuendada nüüd?",
|
"content": "Uuendada nüüd?",
|
||||||
"updateNow": "Uuenda nüüd",
|
"updateNow": "Uuenda nüüd",
|
||||||
"updateLater": "Hiljem",
|
"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": {
|
"country": {
|
||||||
"cn": "Hiina",
|
"cn": "Hiina",
|
||||||
|
|||||||
@@ -209,7 +209,7 @@
|
|||||||
"rewardDetails": "報酬の詳細 >",
|
"rewardDetails": "報酬の詳細 >",
|
||||||
"steps": "招待の手順",
|
"steps": "招待の手順",
|
||||||
"inviteFriend": "友達を招待",
|
"inviteFriend": "友達を招待",
|
||||||
"acceptInvite": "友達が招待を受け入れ\n注文して登録",
|
"acceptInvite": "友達が招待を受け入れ注文して登録",
|
||||||
"getReward": "報酬を獲得",
|
"getReward": "報酬を獲得",
|
||||||
"shareLink": "リンクを共有",
|
"shareLink": "リンクを共有",
|
||||||
"shareQR": "QRコードを共有",
|
"shareQR": "QRコードを共有",
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
"content": "今すぐアップデートしますか?",
|
"content": "今すぐアップデートしますか?",
|
||||||
"updateNow": "今すぐアップデート",
|
"updateNow": "今すぐアップデート",
|
||||||
"updateLater": "後で",
|
"updateLater": "後で",
|
||||||
"defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上"
|
"defaultContent": "1. アプリのパフォーマンス最適化2. 既知の問題の修正3. ユーザー体験の向上"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "中国",
|
"cn": "中国",
|
||||||
|
|||||||
@@ -387,7 +387,7 @@
|
|||||||
"content": "Хотите обновить сейчас?",
|
"content": "Хотите обновить сейчас?",
|
||||||
"updateNow": "Обновить сейчас",
|
"updateNow": "Обновить сейчас",
|
||||||
"later": "Позже",
|
"later": "Позже",
|
||||||
"defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта"
|
"defaultContent": "1. Оптимизация производительности приложения2. Исправление известных проблем3. Улучшение пользовательского опыта"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "Китай",
|
"cn": "Китай",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"welcome": "欢迎使用 Hi快VPN",
|
"welcome": "欢迎使用 Hi快VPN",
|
||||||
"disconnected": "未连接",
|
"disconnected": "未连接",
|
||||||
"connecting": "正在连接",
|
"connecting": "正在连接...",
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
"disconnecting": "正在断开",
|
"disconnecting": "正在断开",
|
||||||
"currentConnectionTitle": "当前连接",
|
"currentConnectionTitle": "当前连接",
|
||||||
@@ -232,13 +232,13 @@
|
|||||||
"longestConnection": "最长连接时间",
|
"longestConnection": "最长连接时间",
|
||||||
"days": "{days}天",
|
"days": "{days}天",
|
||||||
"daysOfWeek": {
|
"daysOfWeek": {
|
||||||
"monday": "周\n一",
|
"monday": "周一",
|
||||||
"tuesday": "周\n二",
|
"tuesday": "周二",
|
||||||
"wednesday": "周\n三",
|
"wednesday": "周三",
|
||||||
"thursday": "周\n四",
|
"thursday": "周四",
|
||||||
"friday": "周\n五",
|
"friday": "周五",
|
||||||
"saturday": "周\n六",
|
"saturday": "周六",
|
||||||
"sunday": "周\n日"
|
"sunday": "周日"
|
||||||
},
|
},
|
||||||
"processTrafficFailed": "处理流量日志数据失败"
|
"processTrafficFailed": "处理流量日志数据失败"
|
||||||
},
|
},
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
"rewardDetails": "奖励明细 >",
|
"rewardDetails": "奖励明细 >",
|
||||||
"steps": "邀请步骤",
|
"steps": "邀请步骤",
|
||||||
"inviteFriend": "邀请好友",
|
"inviteFriend": "邀请好友",
|
||||||
"acceptInvite": "好友接受邀请\n下单并注册",
|
"acceptInvite": "好友接受邀请下单并注册",
|
||||||
"getReward": "获得奖励",
|
"getReward": "获得奖励",
|
||||||
"shareLink": "分享链接",
|
"shareLink": "分享链接",
|
||||||
"shareQR": "分享二维码",
|
"shareQR": "分享二维码",
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
"content": "是否立即更新?",
|
"content": "是否立即更新?",
|
||||||
"updateNow": "立即更新",
|
"updateNow": "立即更新",
|
||||||
"updateLater": "稍后再说",
|
"updateLater": "稍后再说",
|
||||||
"defaultContent": "1. 优化应用性能\n2. 修复已知问题\n3. 提升用户体验"
|
"defaultContent": "1. 优化应用性能2. 修复已知问题3. 提升用户体验"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "中国",
|
"cn": "中国",
|
||||||
|
|||||||
@@ -170,13 +170,13 @@
|
|||||||
"longestConnection": "最長連接時間",
|
"longestConnection": "最長連接時間",
|
||||||
"days": "{days}天",
|
"days": "{days}天",
|
||||||
"daysOfWeek": {
|
"daysOfWeek": {
|
||||||
"monday": "週\n一",
|
"monday": "週一",
|
||||||
"tuesday": "週\n二",
|
"tuesday": "週二",
|
||||||
"wednesday": "週\n三",
|
"wednesday": "週三",
|
||||||
"thursday": "週\n四",
|
"thursday": "週四",
|
||||||
"friday": "週\n五",
|
"friday": "週五",
|
||||||
"saturday": "週\n六",
|
"saturday": "週六",
|
||||||
"sunday": "週\n日"
|
"sunday": "週日"
|
||||||
},
|
},
|
||||||
"processTrafficFailed": "處理流量日誌資料失敗"
|
"processTrafficFailed": "處理流量日誌資料失敗"
|
||||||
},
|
},
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
"rewardDetails": "獎勵明細 >",
|
"rewardDetails": "獎勵明細 >",
|
||||||
"steps": "邀請步驟",
|
"steps": "邀請步驟",
|
||||||
"inviteFriend": "邀請好友",
|
"inviteFriend": "邀請好友",
|
||||||
"acceptInvite": "好友接受邀請\n下單並註冊",
|
"acceptInvite": "好友接受邀請下單並註冊",
|
||||||
"getReward": "獲得獎勵",
|
"getReward": "獲得獎勵",
|
||||||
"shareLink": "分享連結",
|
"shareLink": "分享連結",
|
||||||
"shareQR": "分享二維碼",
|
"shareQR": "分享二維碼",
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
"content": "是否立即更新?",
|
"content": "是否立即更新?",
|
||||||
"updateNow": "立即更新",
|
"updateNow": "立即更新",
|
||||||
"updateLater": "稍後",
|
"updateLater": "稍後",
|
||||||
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
|
"defaultContent": "1. 優化應用性能2. 修復已知問題3. 改進用戶體驗"
|
||||||
},
|
},
|
||||||
"orderStatus": {
|
"orderStatus": {
|
||||||
"title": "訂單狀態",
|
"title": "訂單狀態",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ targets:
|
|||||||
options:
|
options:
|
||||||
explicit_to_json: true
|
explicit_to_json: true
|
||||||
drift_dev:
|
drift_dev:
|
||||||
|
generate_for:
|
||||||
|
- lib/**.drift
|
||||||
options:
|
options:
|
||||||
store_date_time_values_as_text: true
|
store_date_time_values_as_text: true
|
||||||
slang_build_runner:
|
slang_build_runner:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
name: BearVPN
|
name: BearVPN
|
||||||
|
app_name: BearVPN
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
build_number: 1
|
build_number: 1
|
||||||
targets:
|
targets:
|
||||||
@@ -11,3 +12,7 @@ targets:
|
|||||||
enable: true
|
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.
|
// 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
|
SERVICE_IDENTIFIER=com.hi.app
|
||||||
DEVELOPMENT_TEAM=3UR892FAP3
|
DEVELOPMENT_TEAM=NJRRF427XB
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.app.hi.com</string>
|
<string>group.$(BASE_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -758,11 +758,11 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
|
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
EXCLUDED_ARCHS = armv7;
|
EXCLUDED_ARCHS = armv7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -788,7 +788,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = dev_p;
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
@@ -814,9 +814,11 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnelRelease.entitlements;
|
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnelRelease.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
EXCLUDED_ARCHS = armv7;
|
EXCLUDED_ARCHS = armv7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -842,6 +844,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
@@ -865,9 +868,11 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
|
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
EXCLUDED_ARCHS = armv7;
|
EXCLUDED_ARCHS = armv7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -893,6 +898,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
@@ -933,7 +939,7 @@
|
|||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = 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;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
@@ -975,7 +981,7 @@
|
|||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -1004,9 +1010,10 @@
|
|||||||
"-lresolv",
|
"-lresolv",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = hot;
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -1023,7 +1030,7 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 3UR892FAP3;
|
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
||||||
@@ -1042,7 +1049,7 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 3UR892FAP3;
|
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
||||||
@@ -1059,7 +1066,7 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 3UR892FAP3;
|
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
|
||||||
@@ -1161,7 +1168,7 @@
|
|||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = 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;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
@@ -1201,11 +1208,11 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
||||||
@@ -1234,9 +1241,10 @@
|
|||||||
"-lresolv",
|
"-lresolv",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = dev;
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -1257,9 +1265,11 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -1288,8 +1298,10 @@
|
|||||||
"-lresolv",
|
"-lresolv",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
|||||||
@@ -52,10 +52,10 @@
|
|||||||
<string>需要相机权限以支持拍照功能</string>
|
<string>需要相机权限以支持拍照功能</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>需要麦克风权限以支持语音消息功能</string>
|
<string>需要麦克风权限以支持语音消息功能</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>需要相册权限以支持图片上传功能</string>
|
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>需要相册添加权限以保存图片</string>
|
<string>需要相册添加权限以保存图片</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>需要相册权限以支持图片上传功能</string>
|
||||||
<key>SERVICE_IDENTIFIER</key>
|
<key>SERVICE_IDENTIFIER</key>
|
||||||
<string>$(SERVICE_IDENTIFIER)</string>
|
<string>$(SERVICE_IDENTIFIER)</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.app.hi.com</string>
|
<string>group.com.taw.hifastvpn</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.app.hi.com</string>
|
<string>group.com.taw.hifastvpn</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -10,19 +10,20 @@
|
|||||||
<string>app-store</string>
|
<string>app-store</string>
|
||||||
<key>provisioningProfiles</key>
|
<key>provisioningProfiles</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>app.myhiddify.com</key>
|
<key>com.taw.hifastvpn</key>
|
||||||
<string>dist.apple.app.myhiddify.com</string>
|
<string>HiFastVPN-iOS-Pord</string>
|
||||||
<key>app.myhiddify.com.SingBoxPacketTunnel</key>
|
|
||||||
<string>dist.apple.app.myhiddify.com.singboxpackettunnel</string>
|
<key>com.taw.hifastvpn.PacketTunnel</key>
|
||||||
|
<string>HiFastVPN-iOS-Pord-PacketTunnel</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>signingCertificate</key>
|
<key>signingCertificate</key>
|
||||||
<string>E2217AF6F3AD11BA09F9FD95E025D7E637F8B081</string>
|
<string>545725EC39FB3B586B4F63F4BF8D9834FDCFE946</string>
|
||||||
<key>signingStyle</key>
|
<key>signingStyle</key>
|
||||||
<string>manual</string>
|
<string>manual</string>
|
||||||
<key>stripSwiftSymbols</key>
|
<key>stripSwiftSymbols</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>teamID</key>
|
<key>teamID</key>
|
||||||
<string>3UR892FAP3</string>
|
<string>NJRRF427XB</string>
|
||||||
<key>uploadSymbols</key>
|
<key>uploadSymbols</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -1309,6 +1309,9 @@ class AppConfig {
|
|||||||
/// 网站ID
|
/// 网站ID
|
||||||
String kr_website_id = "";
|
String kr_website_id = "";
|
||||||
|
|
||||||
|
/// 设备限制数量
|
||||||
|
String device_limit = '0';
|
||||||
|
|
||||||
/// 是否为白天模式
|
/// 是否为白天模式
|
||||||
bool kr_is_daytime = true;
|
bool kr_is_daytime = true;
|
||||||
|
|
||||||
@@ -1447,7 +1450,7 @@ class AppConfig {
|
|||||||
kr_official_telegram = config.kr_official_telegram;
|
kr_official_telegram = config.kr_official_telegram;
|
||||||
kr_official_telephone = config.kr_official_telephone;
|
kr_official_telephone = config.kr_official_telephone;
|
||||||
kr_invitation_link = config.kr_invitation_link;
|
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) {
|
if (config.kr_domains.isNotEmpty) {
|
||||||
KRDomain.kr_handleDomains(config.kr_domains);
|
KRDomain.kr_handleDomains(config.kr_domains);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,10 @@ class KROutboundItem {
|
|||||||
|
|
||||||
/// URL
|
/// URL
|
||||||
String url = "";
|
String url = "";
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)';
|
|
||||||
}
|
|
||||||
/// 服务器类型
|
/// 服务器类型
|
||||||
|
|
||||||
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
|
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
||||||
KROutboundItem(KrNodeListItem nodeListItem) {
|
KROutboundItem(KrNodeListItem nodeListItem) {
|
||||||
id = nodeListItem.id.toString();
|
id = nodeListItem.id.toString();
|
||||||
protocol = nodeListItem.protocol;
|
protocol = nodeListItem.protocol;
|
||||||
@@ -274,6 +271,7 @@ class KROutboundItem {
|
|||||||
// 🔧 尝试从 config 字段解析 transport 配置
|
// 🔧 尝试从 config 字段解析 transport 配置
|
||||||
Map<String, dynamic>? transportConfig;
|
Map<String, dynamic>? transportConfig;
|
||||||
Map<String, dynamic>? securityConfig;
|
Map<String, dynamic>? securityConfig;
|
||||||
|
int actualPort = nodeListItem.port; // 🔧 使用局部变量存储实际端口
|
||||||
|
|
||||||
// 🔧 关键修复:优先从 protocols 字段解析配置
|
// 🔧 关键修复:优先从 protocols 字段解析配置
|
||||||
if (nodeListItem.protocols.isNotEmpty) {
|
if (nodeListItem.protocols.isNotEmpty) {
|
||||||
@@ -307,6 +305,28 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedProtocol != null) {
|
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 配置
|
// 提取 transport 配置
|
||||||
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
||||||
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
||||||
@@ -411,18 +431,17 @@ class KROutboundItem {
|
|||||||
"type": "shadowsocks",
|
"type": "shadowsocks",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"method": finalMethod,
|
"method": finalMethod,
|
||||||
"password": nodeListItem.uuid
|
"password": nodeListItem.uuid
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('📄 使用加密方法: $finalMethod');
|
print('📄 使用加密方法: $finalMethod');
|
||||||
}
|
print('📄 完整配置 JSON:');
|
||||||
if (kDebugMode) {
|
print(jsonEncode(config));
|
||||||
print('📄 完整配置: $config');
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "vless":
|
case "vless":
|
||||||
@@ -436,17 +455,25 @@ class KROutboundItem {
|
|||||||
serverName = securityConfig['sni'].toString();
|
serverName = securityConfig['sni'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
// 🔧 关键修复:智能判断是否启用 TLS
|
||||||
final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
// 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) {
|
if (kDebugMode) {
|
||||||
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled');
|
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vless",
|
"type": "vless",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"uuid": nodeListItem.uuid,
|
"uuid": nodeListItem.uuid,
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
if (vlessTlsEnabled) "tls": {
|
if (vlessTlsEnabled) "tls": {
|
||||||
@@ -460,10 +487,23 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
||||||
}
|
print('📋 原始节点信息:');
|
||||||
if (kDebugMode) {
|
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||||
print('📄 完整配置: $config');
|
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;
|
break;
|
||||||
case "vmess":
|
case "vmess":
|
||||||
@@ -477,17 +517,25 @@ class KROutboundItem {
|
|||||||
serverName = securityConfig['sni'].toString();
|
serverName = securityConfig['sni'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
// 🔧 关键修复:智能判断是否启用 TLS
|
||||||
final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
// 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) {
|
if (kDebugMode) {
|
||||||
print('🔐 TLS 状态: enabled=$tlsEnabled');
|
print('🔐 VMess TLS 状态: enabled=$tlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vmess",
|
"type": "vmess",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"uuid": nodeListItem.uuid,
|
"uuid": nodeListItem.uuid,
|
||||||
"alter_id": 0,
|
"alter_id": 0,
|
||||||
"security": "auto",
|
"security": "auto",
|
||||||
@@ -503,10 +551,23 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
||||||
}
|
print('📋 原始节点信息:');
|
||||||
if (kDebugMode) {
|
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||||
print('📄 完整配置: $config');
|
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;
|
break;
|
||||||
case "trojan":
|
case "trojan":
|
||||||
@@ -524,7 +585,7 @@ class KROutboundItem {
|
|||||||
"type": "trojan",
|
"type": "trojan",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"password": nodeListItem.uuid,
|
"password": nodeListItem.uuid,
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
"tls": {
|
"tls": {
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ class KRIsRegister {
|
|||||||
KRIsRegister({this.kr_isRegister = false});
|
KRIsRegister({this.kr_isRegister = false});
|
||||||
|
|
||||||
KRIsRegister.fromJson(Map<String, dynamic> json) {
|
KRIsRegister.fromJson(Map<String, dynamic> json) {
|
||||||
kr_isRegister = json['Status'] == "true" || json['Status'] == true
|
kr_isRegister = json['exist'] == "true" || json['exist'] == true
|
||||||
? true
|
? true
|
||||||
: false || json['status'] == "true" || json['status'] == true
|
: false || json['exist'] == "true" || json['exist'] == true
|
||||||
? true
|
? true
|
||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
data['Status'] = kr_isRegister;
|
data['exist'] = kr_isRegister;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,25 +136,68 @@ class KrNodeListItem {
|
|||||||
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
||||||
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
|
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) {
|
if (protocols.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final protocolsList = jsonDecode(protocols) as List;
|
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) {
|
if (protocolsList.isNotEmpty) {
|
||||||
final firstProtocol = protocolsList[0] as Map<String, dynamic>;
|
// 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个
|
||||||
// 优先使用 protocols 中的配置
|
Map<String, dynamic>? matchedProtocolConfig;
|
||||||
if (firstProtocol['port'] != null) {
|
|
||||||
port = _parseIntSafely(firstProtocol['port']);
|
// 尝试找到协议类型匹配的配置
|
||||||
|
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();
|
|
||||||
}
|
}
|
||||||
KRLogUtil.kr_i('从 protocols 解析: port=$port, cipher=$method', tag: 'NodeList');
|
|
||||||
|
// 如果没找到匹配的,使用第一个配置(兼容旧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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_w('解析 protocols 字段失败: $e', tag: 'NodeList');
|
KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
|
||||||
|
|
||||||
return KrNodeListItem(
|
return KrNodeListItem(
|
||||||
id: _parseIntSafely(json['id']),
|
id: _parseIntSafely(json['id']),
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class KRSiteInfo {
|
|||||||
final String customHtml;
|
final String customHtml;
|
||||||
final String customData;
|
final String customData;
|
||||||
final String crispId;
|
final String crispId;
|
||||||
|
final String deviceLimit;
|
||||||
|
|
||||||
KRSiteInfo({
|
KRSiteInfo({
|
||||||
required this.host,
|
required this.host,
|
||||||
@@ -73,10 +74,12 @@ class KRSiteInfo {
|
|||||||
required this.customHtml,
|
required this.customHtml,
|
||||||
required this.customData,
|
required this.customData,
|
||||||
required this.crispId,
|
required this.crispId,
|
||||||
|
required this.deviceLimit,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory KRSiteInfo.fromJson(Map<String, dynamic> json) {
|
factory KRSiteInfo.fromJson(Map<String, dynamic> json) {
|
||||||
String crispId = '0';
|
String crispId = '0';
|
||||||
|
String deviceLimit = '0';
|
||||||
|
|
||||||
// 尝试解析 custom_data 中的 kr_website_id
|
// 尝试解析 custom_data 中的 kr_website_id
|
||||||
try {
|
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(
|
return KRSiteInfo(
|
||||||
host: json['host'] ?? '',
|
host: json['host'] ?? '',
|
||||||
siteName: json['site_name'] ?? '',
|
siteName: json['site_name'] ?? '',
|
||||||
@@ -98,6 +111,7 @@ class KRSiteInfo {
|
|||||||
customHtml: json['custom_html'] ?? '',
|
customHtml: json['custom_html'] ?? '',
|
||||||
customData: json['custom_data'] ?? '',
|
customData: json['custom_data'] ?? '',
|
||||||
crispId: crispId,
|
crispId: crispId,
|
||||||
|
deviceLimit: deviceLimit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,10 +37,12 @@ class HIMenuView extends GetView<HIMenuController> {
|
|||||||
mainAxisSize: MainAxisSize.min, // 让 Column 包裹内容
|
mainAxisSize: MainAxisSize.min, // 让 Column 包裹内容
|
||||||
children: [
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final account = KRAppRunData.getInstance().kr_account.value ?? '未登录';
|
final account = KRAppRunData.getInstance().kr_account.value;
|
||||||
|
final isDeviceLogin = account != null && account.startsWith('9000');
|
||||||
|
final accountText = isDeviceLogin ? '待绑定' : '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||||
return UserInfoCard(
|
return UserInfoCard(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
userId: account,
|
userId: accountText,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -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/modules/hi_menu/controllers/hi_menu_controller.dart';
|
||||||
import 'package:kaer_with_panels/app/routes/app_pages.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_local_image.dart';
|
||||||
|
import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart';
|
||||||
|
|
||||||
/// 用户信息展示卡片 Widget
|
/// 用户信息展示卡片 Widget
|
||||||
///
|
///
|
||||||
@@ -58,58 +59,19 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'ID: $userId',
|
userId,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Obx(() {
|
KRSubscriptionExpiryText(
|
||||||
final currentSubscribe =
|
expireTimeProvider: () => controller.kr_subscribeService
|
||||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
.kr_currentSubscribe.value?.expireTime,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import 'package:get/get.dart';
|
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/utils/kr_log_util.dart';
|
||||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.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/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 '../../../localization/app_translations.dart';
|
||||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.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 KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||||
|
|
||||||
/// 首页服务
|
/// 首页服务
|
||||||
final KRHomeController homeController = Get.find<KRHomeController>();
|
final KRHomeController homeController = Get.find<KRHomeController>();
|
||||||
|
|
||||||
|
/// 调试模式状态
|
||||||
|
final RxBool isDebugMode = false.obs;
|
||||||
|
|
||||||
|
/// 模式按钮点击计数器
|
||||||
|
int modeButtonClickCount = 0;
|
||||||
|
|
||||||
|
/// 最后一次点击时间
|
||||||
|
DateTime? lastModeButtonClickTime;
|
||||||
|
|
||||||
/// 获取连接类型字符串
|
/// 获取连接类型字符串
|
||||||
String kr_getConnectionTypeString() {
|
String kr_getConnectionTypeString() {
|
||||||
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
||||||
@@ -30,15 +41,87 @@ class HINodeListController extends GetxController {
|
|||||||
KRLogUtil.kr_i('连接类型已更新为: $type', tag: 'HINodeListController');
|
KRLogUtil.kr_i('连接类型已更新为: $type', tag: 'HINodeListController');
|
||||||
// 这里可以添加其他需要的逻辑
|
// 这里可以添加其他需要的逻辑
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果当前有选择的国家,触发重选检查
|
/// 处理模式按钮点击(用于激活调试模式)
|
||||||
if (homeController.currentSelectedCountry.isNotEmpty) {
|
void kr_handleModeButtonClick() {
|
||||||
KRLogUtil.kr_i('连接类型更新后,检查是否需要重选节点', tag: 'HINodeListController');
|
final now = DateTime.now();
|
||||||
// 延迟一下让连接类型更新完成
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
// 检查是否在2秒内连续点击
|
||||||
// homeController.checkCountryReselection(KRSingBoxImp.instance.kr_activeGroups);
|
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
|
// hi_node_list_view.dart
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
|
||||||
import '../../../model/business/kr_outbound_item.dart';
|
import '../../../model/business/kr_outbound_item.dart';
|
||||||
import '../../../utils/kr_log_util.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 '../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/modules/kr_home/models/kr_home_views_status.dart';
|
||||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||||
@@ -24,22 +24,6 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
static const Color krModernGreen = Color(0xFF4CAF50);
|
static const Color krModernGreen = Color(0xFF4CAF50);
|
||||||
static const Color krModernGreenLight = Color(0xFF81C784);
|
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)
|
/// 获取分组内最快节点的延迟值(单位:ms)
|
||||||
/// 如果列表为空,返回 0
|
/// 如果列表为空,返回 0
|
||||||
int getFastestNodeDelay(
|
int getFastestNodeDelay(
|
||||||
@@ -48,17 +32,10 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
) {
|
) {
|
||||||
if (outboundList.isEmpty) return 0;
|
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));
|
outboundList
|
||||||
return validNodes.first.urlTestDelay.value;
|
.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) {
|
Widget build(BuildContext context) {
|
||||||
// 1. 使用 Material 作为根组件,确保 InkWell 的水波纹效果正常
|
// 1. 使用 Material 作为根组件,确保 InkWell 的水波纹效果正常
|
||||||
// 并设置透明背景,让父组件的背景可以透出来
|
// 并设置透明背景,让父组件的背景可以透出来
|
||||||
|
return Obx(() {
|
||||||
|
// Material 保持透明背景,让 InkWell 水波纹正常
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: _buildSubscribeList(context)
|
child: controller.isDebugMode.value
|
||||||
// child: _kr_buildRegionList(context)
|
? _buildSubscribeList(context)
|
||||||
|
: _kr_buildRegionList(context),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建国家/地区分组列表
|
/// 构建国家/地区分组列表
|
||||||
Widget _kr_buildRegionList(BuildContext context) {
|
Widget _kr_buildRegionList(BuildContext context) {
|
||||||
return Obx(() {
|
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(
|
return _kr_buildListContainer(
|
||||||
context,
|
context,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@@ -110,21 +84,23 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||||
children: [
|
children: [
|
||||||
if (controller.kr_subscribeService.countryOutboundList.isEmpty)
|
if (controller.kr_subscribeService.countryOutboundList.isEmpty)
|
||||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noRegions)
|
_buildEmptyListPlaceholder(
|
||||||
|
context, AppTranslations.kr_home.noRegions)
|
||||||
else ...[
|
else ...[
|
||||||
InkWell(
|
InkWell(
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
try {
|
try {
|
||||||
final success =
|
controller.homeController
|
||||||
await controller.homeController.kr_performNodeSwitch('auto');
|
.kr_setSelectedCountryTag('auto');
|
||||||
|
final success = await controller.homeController
|
||||||
|
.kr_performNodeSwitch('auto');
|
||||||
if (success) {
|
if (success) {
|
||||||
controller.homeController.kr_currentListStatus.value =
|
controller.homeController.kr_currentListStatus.value =
|
||||||
KRHomeViewsListStatus.kr_none;
|
KRHomeViewsListStatus.kr_none;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_e('Auto选项切换异常: $e',
|
KRLogUtil.kr_e('Auto选项切换异常: $e', tag: 'NodeListView');
|
||||||
tag: 'NodeListView');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
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(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -180,7 +157,9 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Obx(() => controller.homeController.kr_coutryText.value == 'auto'
|
Obx(() =>
|
||||||
|
controller.homeController.kr_selectedCountryTag.value ==
|
||||||
|
'auto'
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
@@ -197,11 +176,23 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
...controller.kr_subscribeService.countryOutboundList
|
||||||
|
.map((country) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
// 自动选择这个国家下的节点延迟中最快的
|
try {
|
||||||
controller.homeController.onCountrySelected(country.country);
|
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),
|
child: _kr_buildCountryListItem(context, country: country),
|
||||||
);
|
);
|
||||||
@@ -212,31 +203,30 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建默认的订阅节点列表
|
/// 构建默认的订阅节点列表
|
||||||
Widget _buildSubscribeList(BuildContext context) {
|
Widget _buildSubscribeList(BuildContext context) {
|
||||||
return Obx(() {
|
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(
|
return _kr_buildListContainer(
|
||||||
context,
|
context,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||||
children: [
|
children: [
|
||||||
if (controller.kr_subscribeService.allList.isEmpty)
|
...[
|
||||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
|
||||||
else ...[
|
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||||
controller.homeController.kr_selectNode('auto');
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
final success = await controller.homeController
|
||||||
|
.kr_performNodeSwitch('auto');
|
||||||
|
if (success) {
|
||||||
controller.homeController.kr_currentListStatus.value =
|
controller.homeController.kr_currentListStatus.value =
|
||||||
KRHomeViewsListStatus.kr_none;
|
KRHomeViewsListStatus.kr_none;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('Auto选项切换异常: $e', tag: 'NodeListView');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -247,7 +237,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(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -282,16 +273,39 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||||
Text(
|
Text(
|
||||||
'根据网络IP自动匹配最快线路', // 您指定的文本
|
'根据网络IP自动匹配最快线路', // 默认文本
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
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'
|
Obx(() =>
|
||||||
|
controller.homeController.kr_cutTag.value == 'auto'
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
@@ -308,9 +322,22 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...controller.kr_subscribeService.allList.map((item) {
|
...controller.kr_subscribeService.allList().map((item) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => _onNodeSelected(item),
|
// 🔧 修复:改为 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),
|
child: _kr_buildNodeListItem(context, item: item),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -328,7 +355,8 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
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
|
/// 构建单个节点列表项的UI
|
||||||
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
Widget _kr_buildNodeListItem(BuildContext context,
|
||||||
|
{required KROutboundItem item}) {
|
||||||
return Container(
|
return Container(
|
||||||
key: ValueKey(item.id),
|
key: ValueKey(item.id),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -369,71 +389,92 @@ 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(
|
child: Row(
|
||||||
children: [
|
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),
|
SizedBox(width: 12.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Obx(() {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final isDebug = controller.isDebugMode.value;
|
||||||
children: [
|
|
||||||
Row(
|
final text = isDebug
|
||||||
children: [
|
? '${controller.homeController.kr_getCountryFullName(item.country)} - ${item.tag}'
|
||||||
Flexible(
|
: '${controller.homeController.kr_getCountryFullName(item.country)}';
|
||||||
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()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 2.w),
|
|
||||||
Obx(() {
|
|
||||||
// 2. 获取用于显示的延迟值
|
|
||||||
final int delay = _getDisplayDelay(controller, item);
|
|
||||||
return Text(
|
return Text(
|
||||||
'${delay}ms',
|
text,
|
||||||
|
style: KrAppTextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
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(
|
style: KrAppTextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: krModernGreen,
|
color: delayColor, // 使用动态计算出的颜色
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Text(
|
SizedBox(width: 12.w),
|
||||||
item.city,
|
Obx(() {
|
||||||
style: KrAppTextStyle(fontSize: 12, color: Theme.of(context).textTheme.bodySmall?.color),
|
final selected =
|
||||||
),
|
controller.homeController.kr_cutSeletedTag.value == item.tag;
|
||||||
],
|
return selected
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
width: 16.w,
|
||||||
height: 16.h,
|
height: 16.h,
|
||||||
)
|
)
|
||||||
: KrLocalImage(
|
: KrLocalImage(
|
||||||
imageName: 'radio-icon',
|
imageName: 'radio-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
width: 16.w,
|
||||||
height: 16.h,
|
height: 16.h,
|
||||||
)),
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -441,18 +482,6 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
|
|
||||||
/// 构建国家列表项的UI
|
/// 构建国家列表项的UI
|
||||||
Widget _kr_buildCountryListItem(BuildContext context, {required country}) {
|
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(
|
return Container(
|
||||||
key: ValueKey(country),
|
key: ValueKey(country),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -467,43 +496,95 @@ 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(
|
child: Row(
|
||||||
children: [
|
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),
|
SizedBox(width: 12.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.homeController.kr_getCountryFullName(country.country),
|
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(() {
|
Obx(() {
|
||||||
final int delay = getFastestNodeDelay(controller, country.outboundList);
|
// 1. 获取延迟值和测速状态
|
||||||
return Text(
|
int displayDelay =
|
||||||
delay == 0
|
getFastestNodeDelay(controller, country.outboundList);
|
||||||
? ''
|
bool isTesting = controller.homeController.kr_isLatency.value;
|
||||||
: delay >= 3000
|
|
||||||
? AppTranslations.kr_home.timeout
|
// 2. 声明文本和颜色变量
|
||||||
: '${delay}ms',
|
String delayText;
|
||||||
style: KrAppTextStyle(
|
Color delayColor;
|
||||||
fontSize: 10,
|
|
||||||
color: getLatencyColor(delay),
|
// 3. 根据状态设置文本和颜色
|
||||||
fontWeight: FontWeight.w500,
|
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),
|
SizedBox(width: 12.w),
|
||||||
Obx(() => controller.homeController.kr_coutryText.value == country.country
|
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(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
width: 16.w,
|
||||||
height: 16.h,
|
height: 16.h,
|
||||||
)
|
)
|
||||||
: KrLocalImage(
|
: KrLocalImage(
|
||||||
imageName: 'radio-icon',
|
imageName: 'radio-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
width: 16.w,
|
||||||
height: 16.h,
|
height: 16.h,
|
||||||
)),
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,10 +19,47 @@ class HINodePageView extends GetView<HINodeListController> {
|
|||||||
return HIBaseScaffold(
|
return HIBaseScaffold(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
// 模式切换器
|
// 模式切换器
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
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预留空间
|
padding: EdgeInsets.only(left: 60.w, right: 60.w, bottom: 90.w), // 为HIHelpEntrance预留空间
|
||||||
// 2. 在 Padding 内部使用 EasyRefresh
|
// 2. 在 Padding 内部使用 EasyRefresh
|
||||||
child: EasyRefresh(
|
child: EasyRefresh(
|
||||||
// 3. 绑定 onRefresh 回调
|
onRefresh: controller.kr_handleRefresh,
|
||||||
onRefresh: controller.kr_subscribeService.kr_refreshAll,
|
|
||||||
// 4. 自定义 Header 样式
|
// 4. 自定义 Header 样式
|
||||||
header: ClassicHeader(
|
header: ClassicHeader(
|
||||||
dragText: '下拉刷新',
|
dragText: '下拉刷新',
|
||||||
@@ -159,7 +195,12 @@ class HINodePageView extends GetView<HINodeListController> {
|
|||||||
}) {
|
}) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap,
|
onTap: () {
|
||||||
|
// 处理模式按钮点击(用于调试模式激活)
|
||||||
|
controller.kr_handleModeButtonClick();
|
||||||
|
// 执行原有的点击逻辑
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||||
|
|||||||
@@ -222,22 +222,22 @@ class HIUserInfoController extends GetxController {
|
|||||||
String iconName = 'devices';
|
String iconName = 'devices';
|
||||||
|
|
||||||
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
|
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeAndroid;
|
deviceType = 'Android' ;// AppTranslations.kr_deviceManagement.deviceTypeAndroid;
|
||||||
iconName = 'phone_android';
|
iconName = 'phone_android';
|
||||||
} else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) {
|
} 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';
|
iconName = 'phone_iphone';
|
||||||
} else if (userAgent.contains('iPad')) {
|
} else if (userAgent.contains('iPad')) {
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIpad;
|
deviceType = 'iPad';// AppTranslations.kr_deviceManagement.deviceTypeIpad;
|
||||||
iconName = 'tablet';
|
iconName = 'tablet';
|
||||||
} else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) {
|
} 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';
|
iconName = 'desktop_mac';
|
||||||
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
|
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeWindows;
|
deviceType = 'Windows'; //AppTranslations.kr_deviceManagement.deviceTypeWindows;
|
||||||
iconName = 'computer';
|
iconName = 'computer';
|
||||||
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
|
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
|
||||||
deviceType = AppTranslations.kr_deviceManagement.deviceTypeLinux;
|
deviceType = 'Linux';//AppTranslations.kr_deviceManagement.deviceTypeLinux;
|
||||||
iconName = 'computer';
|
iconName = 'computer';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ 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/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/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/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> {
|
class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||||
const HIUserInfoView({super.key});
|
const HIUserInfoView({super.key});
|
||||||
@@ -68,75 +70,33 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final account = KRAppRunData.getInstance().kr_account.value;
|
final account = KRAppRunData.getInstance()
|
||||||
|
.kr_account
|
||||||
|
.value;
|
||||||
|
final isDeviceLogin = account != null &&
|
||||||
|
account.startsWith('9000');
|
||||||
|
final accountText = isDeviceLogin
|
||||||
|
? '待绑定'
|
||||||
|
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||||
return Text(
|
return Text(
|
||||||
(account != null && account.isNotEmpty) ? account : '未绑定',
|
accountText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20.sp,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
height: 0.9,
|
//height: 0.9,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Obx(() {
|
KRSubscriptionExpiryText(
|
||||||
final userId = KRAppRunData.getInstance().kr_userId.value;
|
expireTimeProvider: () => controller
|
||||||
return Text(
|
.kr_subscribeService
|
||||||
'ID: ${(userId != null && userId.toString().isNotEmpty) ? userId : '—'}',
|
.kr_currentSubscribe
|
||||||
style: TextStyle(
|
.value
|
||||||
color: Colors.white.withOpacity(0.85),
|
?.expireTime,
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -146,7 +106,8 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
SizedBox(height: 12.w),
|
SizedBox(height: 12.w),
|
||||||
// 动态:如果已有账号,显示“修改密码”,否则显示“绑定邮箱”
|
// 动态:如果已有账号,显示“修改密码”,否则显示“绑定邮箱”
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final isDeviceLogin = KRAppRunData.getInstance().isDeviceLogin();
|
final isDeviceLogin =
|
||||||
|
KRAppRunData.getInstance().isDeviceLogin();
|
||||||
if (!isDeviceLogin) return SizedBox.shrink();
|
if (!isDeviceLogin) return SizedBox.shrink();
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -163,20 +124,24 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 0.w).copyWith(bottom: 10.w), // 在这里增加底部间距
|
margin: EdgeInsets.symmetric(horizontal: 0)
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
.copyWith(bottom: 10), // 在这里增加底部间距
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(22.w),
|
borderRadius: BorderRadius.circular(22),
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
border:
|
||||||
|
Border.all(color: Colors.white, width: 2),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
isDeviceLogin ? '绑定邮箱' : '修改密码',
|
isDeviceLogin ? '绑定邮箱' : '修改密码',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 16.sp,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -196,7 +161,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
// 使用 Padding 在加载指示器上方添加间距
|
// 使用 Padding 在加载指示器上方添加间距
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(top: 20.w),
|
padding: EdgeInsets.only(top: 20),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
@@ -204,18 +169,20 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 0.w),
|
padding: EdgeInsets.symmetric(horizontal: 0),
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
// 1. 禁止 GridView 自身的滚动,因为它已经在 SingleChildScrollView 内部
|
// 1. 禁止 GridView 自身的滚动,因为它已经在 SingleChildScrollView 内部
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
// 2. 让 GridView 根据内容自动调整高度
|
// 2. 让 GridView 根据内容自动调整高度
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
// 3. 设置网格的配置
|
// 3. 设置网格的配置
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate:
|
||||||
|
SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2, // 强制两列
|
crossAxisCount: 2, // 强制两列
|
||||||
crossAxisSpacing: 10.w, // 水平间距
|
crossAxisSpacing: 10, // 水平间距
|
||||||
mainAxisSpacing: 10.w, // 垂直间距
|
mainAxisSpacing: 10, // 垂直间距
|
||||||
childAspectRatio: 1.5, // 宽高比,需要微调以获得最佳视觉效果
|
mainAxisExtent: 76,
|
||||||
|
// childAspectRatio: 1.5, // 宽高比,需要微调以获得最佳视觉效果
|
||||||
),
|
),
|
||||||
// 4. itemCount 和 itemBuilder 的逻辑保持不变
|
// 4. itemCount 和 itemBuilder 的逻辑保持不变
|
||||||
itemCount: controller.devices.length + 1,
|
itemCount: controller.devices.length + 1,
|
||||||
@@ -229,13 +196,15 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
onDelete: (id) {
|
onDelete: (id) {
|
||||||
HIDialog.show(
|
HIDialog.show(
|
||||||
customMessageWidget: Padding(
|
customMessageWidget: Padding(
|
||||||
padding: EdgeInsets.only(top: 16.w),
|
padding: EdgeInsets.only(top: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'请确认是否移除此设备?',
|
'请确认是否移除此设备?',
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
color:
|
color: Theme.of(context)
|
||||||
Theme.of(context).textTheme.bodyMedium?.color,
|
.textTheme
|
||||||
fontSize: 14.sp,
|
.bodyMedium
|
||||||
|
?.color,
|
||||||
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -247,8 +216,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
},
|
},
|
||||||
onCancel: () {
|
onCancel: () {
|
||||||
controller.deleteDevice(id);
|
controller.deleteDevice(id);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -269,8 +237,10 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
// 👇 核心改动 3: 将固定在底部的按钮放在 Expanded 外部
|
// 👇 核心改动 3: 将固定在底部的按钮放在 Expanded 外部
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if ((KRAppRunData.getInstance().kr_account.value != null &&
|
if ((KRAppRunData.getInstance().kr_account.value != null &&
|
||||||
KRAppRunData.getInstance().kr_account.value!.startsWith('9000')))
|
KRAppRunData.getInstance()
|
||||||
return SizedBox.shrink();
|
.kr_account
|
||||||
|
.value!
|
||||||
|
.startsWith('9000'))) return SizedBox.shrink();
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -285,7 +255,10 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
'注销账号后,所有此账号内的剩余套餐和账户数据将被清空,无法找回。', // 1. 修改为正确的提示信息
|
'注销账号后,所有此账号内的剩余套餐和账户数据将被清空,无法找回。', // 1. 修改为正确的提示信息
|
||||||
textAlign: TextAlign.left, // 文本居中
|
textAlign: TextAlign.left, // 文本居中
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.color,
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -299,8 +272,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
Get.toNamed(Routes.KR_DELETE_ACCOUNT);
|
Get.toNamed(Routes.KR_DELETE_ACCOUNT);
|
||||||
},
|
},
|
||||||
// 4. onConfirm 对应“返回”按钮的点击事件
|
// 4. onConfirm 对应“返回”按钮的点击事件
|
||||||
onConfirm: () {
|
onConfirm: () {},
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -308,7 +280,8 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(24.w),
|
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,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -331,7 +304,10 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
'确认要退出您的账号?', // 1. 修改为正确的提示信息
|
'确认要退出您的账号?', // 1. 修改为正确的提示信息
|
||||||
textAlign: TextAlign.center, // 文本居中
|
textAlign: TextAlign.center, // 文本居中
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.color,
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -340,15 +316,14 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
cancelText: '确认',
|
cancelText: '确认',
|
||||||
confirmText: '返回',
|
confirmText: '返回',
|
||||||
onCancel: () async {
|
onCancel: () async {
|
||||||
final currentDevice = controller.devices.firstWhere(
|
final currentDevice =
|
||||||
|
controller.devices.firstWhere(
|
||||||
(device) => device['is_current'] == true,
|
(device) => device['is_current'] == true,
|
||||||
);
|
);
|
||||||
final deviceId = currentDevice['id'] as String;
|
final deviceId = currentDevice['id'] as String;
|
||||||
await controller.deleteDevice(deviceId);
|
await controller.deleteDevice(deviceId);
|
||||||
},
|
},
|
||||||
onConfirm: () {
|
onConfirm: () {},
|
||||||
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -383,6 +358,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建“当前设备信息卡”的私有方法
|
/// 构建“当前设备信息卡”的私有方法
|
||||||
Widget _buildDeviceCard({
|
Widget _buildDeviceCard({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
@@ -403,13 +379,14 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
final deviceType = deviceInfo['type'] as String;
|
final deviceType = deviceInfo['type'] as String;
|
||||||
final iconName = deviceInfo['icon'] as String;
|
final iconName = deviceInfo['icon'] as String;
|
||||||
|
|
||||||
return Stack( // 使用 Stack 来放置删除按钮
|
return Stack(
|
||||||
|
// 使用 Stack 来放置删除按钮
|
||||||
children: [
|
children: [
|
||||||
// 主体内容容器
|
// 主体内容容器
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.w),
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(24.w), // 可以适当调整圆角
|
borderRadius: BorderRadius.circular(24), // 可以适当调整圆角
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -418,25 +395,25 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(_getIconData(iconName), color: Colors.white, size: 18.w),
|
Icon(_getIconData(iconName), color: Colors.white, size: 18),
|
||||||
SizedBox(width: 6.w),
|
SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'设备:${_extractDeviceModel(userAgent)}',
|
'设备:${deviceType}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 10.sp, //
|
fontSize: 10, //
|
||||||
fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.h),
|
SizedBox(height: 4),
|
||||||
Text(
|
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(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.9),
|
color: Colors.white.withOpacity(0.9),
|
||||||
fontSize: 10.sp,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w500),
|
fontWeight: FontWeight.w500),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -490,9 +467,8 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
Widget _buildAddDeviceCard() {
|
Widget _buildAddDeviceCard() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
print('可用设备 2222');
|
|
||||||
print('可用设备 ${controller.kr_subscribeService.kr_currentSubscribe}');
|
|
||||||
// 在这里处理点击“添加设备”的逻辑
|
// 在这里处理点击“添加设备”的逻辑
|
||||||
|
final device_limit = AppConfig.getInstance().device_limit;
|
||||||
HIDialog.show(
|
HIDialog.show(
|
||||||
customMessageWidget: Padding(
|
customMessageWidget: Padding(
|
||||||
padding: EdgeInsets.only(top: 16.w, bottom: 16.w),
|
padding: EdgeInsets.only(top: 16.w, bottom: 16.w),
|
||||||
@@ -502,7 +478,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
children: [
|
children: [
|
||||||
// 2. 第一个 Text 组件
|
// 2. 第一个 Text 组件
|
||||||
Text(
|
Text(
|
||||||
'请使用邮箱登录新设备',
|
'请使用绑定邮箱登录新设备',
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
color: Colors.black, // 可以使用稍浅
|
color: Colors.black, // 可以使用稍浅
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
@@ -510,24 +486,25 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.w),
|
SizedBox(height: 20.w),
|
||||||
Obx(() {
|
Text(
|
||||||
final current = controller.kr_subscribeService.kr_currentSubscribe.value;
|
"每个账号最多允许同时使用${device_limit}台设备同时在线",
|
||||||
|
|
||||||
// 如果 current 为 null,显示默认值 0
|
|
||||||
final deviceLimit = current?.deviceLimit ?? 0;
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
'每个账号最多允许同时使用$deviceLimit台设备同时在线',
|
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
fontWeight: FontWeight.w600,
|
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(
|
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 RegExp regExp = RegExp(r'\((.*?)\)');
|
||||||
final Match? match = regExp.firstMatch(deviceName);
|
final Match? match = regExp.firstMatch(deviceName);
|
||||||
|
|||||||
@@ -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/services/api_service/kr_auth_api.dart';
|
||||||
import 'package:kaer_with_panels/app/utils/kr_common_util.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/services/kr_subscribe_service.dart';
|
||||||
|
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||||
|
|
||||||
import '../../../localization/app_translations.dart';
|
import '../../../localization/app_translations.dart';
|
||||||
|
|
||||||
@@ -65,6 +66,11 @@ class KRDeleteAccountController extends GetxController {
|
|||||||
KRCommonUtil.kr_showToast(error.msg);
|
KRCommonUtil.kr_showToast(error.msg);
|
||||||
},
|
},
|
||||||
(success) {
|
(success) {
|
||||||
|
HIDialog.show(
|
||||||
|
title: '*重要提示',
|
||||||
|
message:
|
||||||
|
'验证邮件已发送至邮箱,如无法找到,请检查垃圾邮件箱或营销邮件箱。',
|
||||||
|
);
|
||||||
kr_canSendCode.value = false;
|
kr_canSendCode.value = false;
|
||||||
kr_countdown.value = 60;
|
kr_countdown.value = 60;
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import 'package:get/get.dart';
|
|||||||
import 'package:kaer_with_panels/app/common/app_run_data.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/hi_base_scaffold.dart';
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.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 '../controllers/kr_delete_account_controller.dart';
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||||
|
|
||||||
|
|
||||||
class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||||
const KRDeleteAccountView({super.key});
|
const KRDeleteAccountView({super.key});
|
||||||
|
|
||||||
@@ -46,7 +46,8 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
|||||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(24.w),
|
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,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -87,7 +88,6 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildUserInfoSection() {
|
Widget _buildUserInfoSection() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -129,12 +129,16 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
final account = KRAppRunData.getInstance()
|
||||||
final userId = (KRAppRunData.getInstance().kr_userId.value ?? '').toString();
|
.kr_account
|
||||||
final deviceId = KRAppRunData.getInstance().deviceId ?? '';
|
.value;
|
||||||
|
final isDeviceLogin = account != null &&
|
||||||
|
account.startsWith('9000');
|
||||||
|
final accountText = isDeviceLogin
|
||||||
|
? '待绑定'
|
||||||
|
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||||
return Text(
|
return Text(
|
||||||
'ID: ${userId.isNotEmpty ? userId : deviceId}',
|
accountText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.85),
|
color: Colors.white.withOpacity(0.85),
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
@@ -142,51 +146,14 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Obx(() {
|
KRSubscriptionExpiryText(
|
||||||
final currentSubscribe =
|
expireTimeProvider: () => controller
|
||||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
.kr_subscribeService.kr_currentSubscribe.value?.expireTime,
|
||||||
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(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -201,6 +168,7 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller.kr_codeController,
|
controller: controller.kr_codeController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -212,7 +180,8 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
|||||||
color: const Color(0xFFA6A6A6),
|
color: const Color(0xFFA6A6A6),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(25.w),
|
borderRadius: BorderRadius.circular(25.w),
|
||||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||||
@@ -232,9 +201,10 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
|||||||
suffixIcon: Padding(
|
suffixIcon: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: controller.kr_canSendCode.value ? controller.kr_sendCode : null,
|
onTap: controller.kr_canSendCode.value
|
||||||
|
? controller.kr_sendCode
|
||||||
|
: null,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100.w,
|
width: 100.w,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
|||||||
|
|
||||||
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 Color buttonColor = Theme.of(context).primaryColor;
|
||||||
final double screenWidth = Get.width;
|
final double screenWidth = Get.width;
|
||||||
@@ -86,8 +86,17 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
|||||||
print('🔵 Switch UI 正在更新,切换中点击了按钮: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
print('🔵 Switch UI 正在更新,切换中点击了按钮: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final hasValidSubscription =
|
final current = controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||||
controller.kr_subscribeService.kr_availableSubscribes.isNotEmpty;
|
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) {
|
if (hasValidSubscription) {
|
||||||
controller.kr_toggleSwitch(!controller.kr_isConnected.value);
|
controller.kr_toggleSwitch(!controller.kr_isConnected.value);
|
||||||
} else {
|
} else {
|
||||||
@@ -291,4 +300,3 @@ class _ContinuousRippleEffectState extends State<ContinuousRippleEffect>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class _HISubscriptionCornerButtonState extends State<HISubscriptionCornerButton>
|
|||||||
if(currentRoute.isEmpty) return
|
if(currentRoute.isEmpty) return
|
||||||
print('🔥 当前路由: $currentRoute');
|
print('🔥 当前路由: $currentRoute');
|
||||||
|
|
||||||
if (currentRoute == Routes.KR_PURCHASE_MEMBERSHIP) {
|
if (currentRoute == Routes.KR_PURCHASE_MEMBERSHIP || currentRoute == Routes.KR_ORDER_STATUS ) {
|
||||||
print('🔥 已经在 KRPurchaseMembership 页面,无需跳转');
|
print('🔥 已经在 KRPurchaseMembership 页面,无需跳转');
|
||||||
return; // 已在目标页面,直接返回
|
return; // 已在目标页面,直接返回
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import 'kr_home_subscription_view.dart';
|
|||||||
import './hi_animated_connect_button.dart';
|
import './hi_animated_connect_button.dart';
|
||||||
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
||||||
|
|
||||||
|
|
||||||
class KRHomeView extends StatefulWidget {
|
class KRHomeView extends StatefulWidget {
|
||||||
const KRHomeView({super.key});
|
const KRHomeView({super.key});
|
||||||
|
|
||||||
@@ -35,8 +34,6 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
controller = Get.find<KRHomeController>();
|
controller = Get.find<KRHomeController>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -56,8 +53,7 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child:
|
child: SafeArea(
|
||||||
SafeArea(
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(40.w, 8.w, 0, 0),
|
padding: EdgeInsets.fromLTRB(40.w, 8.w, 0, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -75,18 +71,21 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
// 订阅信息
|
// 订阅信息
|
||||||
Obx(() {
|
Obx(() {
|
||||||
// 1. 先获取订阅信息
|
// 1. 先获取订阅信息
|
||||||
final currentSubscribe =
|
final currentSubscribe = controller
|
||||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
.kr_subscribeService.kr_currentSubscribe.value;
|
||||||
|
|
||||||
// 2. 准备一个 Widget 变量,用于存放最终要显示的内容
|
// 2. 准备一个 Widget 变量,用于存放最终要显示的内容
|
||||||
Widget content;
|
Widget content;
|
||||||
|
|
||||||
// --- 定义统一的文本样式,并设置行高 ---
|
// --- 定义统一的文本样式,并设置行高 ---
|
||||||
final textStyle = TextStyle(
|
final normalStyle = TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
height: 1.2, // 您可以微调这个值来达到想要的视觉效果
|
height: 1.2,
|
||||||
|
);
|
||||||
|
final highlightStyle = normalStyle.copyWith(
|
||||||
|
color: const Color(0xFFFF00B7),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. 开始条件判断
|
// 3. 开始条件判断
|
||||||
@@ -94,37 +93,41 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
// --- 情况1: 没有任何订阅信息 ---
|
// --- 情况1: 没有任何订阅信息 ---
|
||||||
content = Text(
|
content = Text(
|
||||||
'尚未购买套餐',
|
'尚未购买套餐',
|
||||||
style: textStyle,
|
style: highlightStyle,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// --- 情况2: 有订阅信息,需要判断是否过期 ---
|
// --- 情况2: 有订阅信息,需要判断是否过期 ---
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
DateTime? expireDateTime;
|
DateTime? expireDateTime;
|
||||||
try {
|
try {
|
||||||
expireDateTime = DateTime.parse(currentSubscribe.expireTime);
|
expireDateTime =
|
||||||
|
DateTime.parse(currentSubscribe.expireTime);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 日期格式解析失败
|
// 日期格式解析失败
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expireDateTime != null && expireDateTime.isBefore(now)) {
|
if (expireDateTime != null &&
|
||||||
|
expireDateTime.isBefore(now)) {
|
||||||
// --- 情况2.1: 订阅已过期 ---
|
// --- 情况2.1: 订阅已过期 ---
|
||||||
final formattedExpireDate =
|
final formattedExpireDate =
|
||||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||||
// 使用换行符 \n 合并为单个 Text 组件
|
// 使用换行符 \n 合并为单个 Text 组件
|
||||||
content = Text(
|
content = Text(
|
||||||
'您的套餐已于 $formattedExpireDate 到期\n请前往购买新套餐',
|
'您的套餐已于 $formattedExpireDate 到期\n请前往购买新套餐',
|
||||||
style: textStyle,
|
style: highlightStyle,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// --- 情况2.2: 订阅有效 ---
|
// --- 情况2.2: 订阅有效 ---
|
||||||
final difference = expireDateTime?.difference(now);
|
final difference =
|
||||||
final remainingDaysText = (difference?.inDays ?? 0) > 0
|
expireDateTime?.difference(now);
|
||||||
|
final remainingDaysText =
|
||||||
|
(difference?.inDays ?? 0) > 0
|
||||||
? '${difference!.inDays} 天'
|
? '${difference!.inDays} 天'
|
||||||
: '不足一天';
|
: '不足一天';
|
||||||
// 使用换行符 \n 合并为单个 Text 组件
|
// 使用换行符 \n 合并为单个 Text 组件
|
||||||
content = Text(
|
content = Text(
|
||||||
'套餐剩余:$remainingDaysText\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
|
'套餐剩余:$remainingDaysText\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
|
||||||
style: textStyle,
|
style: normalStyle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,40 +149,45 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
// --- 可点击的 Checkbox 区域 ---
|
// --- 可点击的 Checkbox 区域 ---
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
// 点击方框和文字都可以切换状态
|
// 点击方框和文字都可以切换状态
|
||||||
onTap: () => controller.toggleQuickConnect(!controller.isQuickConnectEnabled.value),
|
onTap: () => controller.toggleQuickConnect(
|
||||||
|
!controller.isQuickConnectEnabled.value),
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 1. 自定义 Checkbox 的方框外观
|
// 1. 自定义 Checkbox 的方框外观
|
||||||
Container(
|
Container(
|
||||||
width: 20.w,
|
width: 20,
|
||||||
height: 20.w,
|
height: 20,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: controller.isQuickConnectEnabled.value
|
color: controller
|
||||||
|
.isQuickConnectEnabled.value
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).primaryColor,
|
color:
|
||||||
|
Theme.of(context).primaryColor,
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(4.r),
|
borderRadius:
|
||||||
|
BorderRadius.circular(4.r),
|
||||||
),
|
),
|
||||||
child: controller.isQuickConnectEnabled.value
|
child: controller
|
||||||
|
.isQuickConnectEnabled.value
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
size: 14.w,
|
size: 14,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
SizedBox(width: 8.w),
|
SizedBox(width: 8),
|
||||||
// 2. “闪连”标签
|
// 2. “闪连”标签
|
||||||
Text(
|
Text(
|
||||||
'闪连',
|
'闪连',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20.sp,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -188,19 +196,20 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// --- 问号图标和点击弹窗区域 ---
|
// --- 问号图标和点击弹窗区域 ---
|
||||||
SizedBox(width: 6.w), // 文字和问号图标之间的间距
|
SizedBox(width: 6), // 文字和问号图标之间的间距
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
HIDialog.show(
|
HIDialog.show(
|
||||||
title: '*闪连功能',
|
title: '*闪连功能',
|
||||||
message: '开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
|
message:
|
||||||
|
'开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: KrLocalImage(
|
child: KrLocalImage(
|
||||||
imageName: 'question-icon', // 图片名称
|
imageName: 'question-icon', // 图片名称
|
||||||
imageType: ImageType.svg, // 图片类型
|
imageType: ImageType.svg, // 图片类型
|
||||||
width: 24.w,
|
width: 24,
|
||||||
height: 24.w,
|
height: 24,
|
||||||
color: Colors.white, // 让图标颜色与边框协调
|
color: Colors.white, // 让图标颜色与边框协调
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class KRInviteView extends GetView<KRInviteController> {
|
|||||||
children: [
|
children: [
|
||||||
// 🟢 第一行:奖励说明
|
// 🟢 第一行:奖励说明
|
||||||
Container(
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
@@ -41,7 +42,7 @@ class KRInviteView extends GetView<KRInviteController> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min, // 由内容撑开
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
KrLocalImage(
|
KrLocalImage(
|
||||||
imageName: 'hi-home-logo',
|
imageName: 'hi-home-logo',
|
||||||
|
|||||||
@@ -305,19 +305,33 @@ class KRLoginController extends GetxController
|
|||||||
|
|
||||||
/// 发送验证码(仅支持邮箱)
|
/// 发送验证码(仅支持邮箱)
|
||||||
void kr_sendCode() async {
|
void kr_sendCode() async {
|
||||||
final either = await KRAuthApi().kr_sendCode(
|
if (accountController.text.isEmpty) {
|
||||||
accountController.text,
|
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
|
||||||
2
|
return;
|
||||||
); // 重置密码验证码类型为3
|
}
|
||||||
/*
|
|
||||||
*
|
int type;
|
||||||
* kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
|
final check = await KRAuthApi().kr_isRegister(accountController.text);
|
||||||
? 2 // 注册验证码类型为2
|
final result = check.fold((l) {
|
||||||
: 3*/
|
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) {
|
either.fold((l) {
|
||||||
KRCommonUtil.kr_showToast(l.msg);
|
KRCommonUtil.kr_showToast(l.msg);
|
||||||
}, (r) async {
|
}, (r) async {
|
||||||
/// 开始倒计时
|
HIDialog.show(
|
||||||
|
title: '*重要提示',
|
||||||
|
message:
|
||||||
|
'验证邮件已发送至邮箱,如无法找到,请检查垃圾邮件箱或营销邮件箱。',
|
||||||
|
);
|
||||||
|
|
||||||
_startCountdown();
|
_startCountdown();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -380,7 +394,13 @@ class KRLoginController extends GetxController
|
|||||||
);
|
);
|
||||||
|
|
||||||
either.fold(
|
either.fold(
|
||||||
(l) => KRCommonUtil.kr_showToast(l.msg),
|
(l) {
|
||||||
|
HIDialog.show(
|
||||||
|
message: l.msg,
|
||||||
|
preventBackDismiss: true,
|
||||||
|
confirmText: '确定',
|
||||||
|
);
|
||||||
|
},
|
||||||
(r) async {
|
(r) async {
|
||||||
_saveLoginData(r);
|
_saveLoginData(r);
|
||||||
KRCommonUtil.kr_showToast('登录成功');
|
KRCommonUtil.kr_showToast('登录成功');
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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/modules/kr_home/controllers/kr_home_controller.dart';
|
||||||
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart';
|
||||||
|
|
||||||
class KRLoginView extends GetView<KRLoginController> {
|
class KRLoginView extends GetView<KRLoginController> {
|
||||||
const KRLoginView({super.key});
|
const KRLoginView({super.key});
|
||||||
@@ -19,7 +20,8 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
|
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(
|
return HIBaseScaffold(
|
||||||
showBack: !isHideBack,
|
showBack: !isHideBack,
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
@@ -27,7 +29,9 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 40.w,),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 40.w,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Text(
|
// Text(
|
||||||
@@ -43,8 +47,7 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!isKeyboardVisible)
|
if (!isKeyboardVisible) const HIHelpEntrance(),
|
||||||
const HIHelpEntrance(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -76,27 +79,17 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
final account = KRAppRunData.getInstance()
|
||||||
final email = KRAppRunData.getInstance().kr_account.value ?? '';
|
.kr_account
|
||||||
return Text(
|
.value;
|
||||||
email.isNotEmpty ? email : '',
|
final isDeviceLogin = account != null &&
|
||||||
style: TextStyle(
|
account.startsWith('9000');
|
||||||
color: Colors.white,
|
final accountText = isDeviceLogin
|
||||||
fontSize: 20.sp,
|
? '待绑定'
|
||||||
fontWeight: FontWeight.bold,
|
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
|
||||||
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 ?? '';
|
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
'ID: ${userId.isNotEmpty ? userId : deviceId}',
|
accountText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.85),
|
color: Colors.white.withOpacity(0.85),
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
@@ -104,51 +97,14 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Obx(() {
|
KRSubscriptionExpiryText(
|
||||||
final currentSubscribe =
|
expireTimeProvider: () => controller
|
||||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
.kr_subscribeService.kr_currentSubscribe.value?.expireTime,
|
||||||
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(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -280,7 +236,8 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
color: const Color(0xFFA6A6A6),
|
color: const Color(0xFFA6A6A6),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(25.w), // 调整为更圆的角
|
borderRadius: BorderRadius.circular(25.w), // 调整为更圆的角
|
||||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||||
@@ -324,7 +281,8 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
color: const Color(0xFFA6A6A6),
|
color: const Color(0xFFA6A6A6),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(25.w),
|
borderRadius: BorderRadius.circular(25.w),
|
||||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||||
@@ -344,9 +302,10 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
suffixIcon: Padding(
|
suffixIcon: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: controller.kr_canSendCode.value ? () => controller.kr_sendCode() : null,
|
onTap: controller.kr_canSendCode.value
|
||||||
|
? () => controller.kr_sendCode()
|
||||||
|
: null,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100.w,
|
width: 100.w,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@@ -375,7 +334,6 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildSaveButton() {
|
Widget _buildSaveButton() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -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 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||||
import '../controllers/kr_order_status_controller.dart';
|
import '../controllers/kr_order_status_controller.dart';
|
||||||
import 'package:kaer_with_panels/app/routes/app_pages.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> {
|
class KROrderStatusView extends GetView<KROrderStatusController> {
|
||||||
@@ -13,52 +15,23 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return HIBaseScaffold(
|
||||||
extendBodyBehindAppBar: true,
|
showBackgroundImage: false,
|
||||||
|
title: AppTranslations.kr_orderStatus.title,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
appBar: AppBar(
|
topContentAreaHeight: 110,
|
||||||
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(
|
child: Obx(
|
||||||
() => SafeArea(
|
() => Stack(
|
||||||
child: Center(
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 60.h),
|
|
||||||
// 状态图标
|
// 状态图标
|
||||||
_buildStatusIcon(),
|
// _buildStatusIcon(),
|
||||||
SizedBox(height: 32.h),
|
SizedBox(height: 122.h),
|
||||||
// 状态文本
|
// 状态文本
|
||||||
_buildStatusText(),
|
_buildStatusText(),
|
||||||
SizedBox(height: 16.h),
|
SizedBox(height: 16.h),
|
||||||
@@ -68,11 +41,13 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
|
|||||||
// 操作按钮(支付成功时显示)
|
// 操作按钮(支付成功时显示)
|
||||||
if (controller.kr_isPaymentSuccess.value)
|
if (controller.kr_isPaymentSuccess.value)
|
||||||
_buildSuccessButtons(),
|
_buildSuccessButtons(),
|
||||||
const Spacer(),
|
SizedBox(height: 100.w),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const HIHelpEntrance(isLight: false),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
|
|||||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// 购买会员页面视图
|
/// 购买会员页面视图
|
||||||
class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
||||||
@@ -27,14 +24,14 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return HIBaseScaffold(
|
return HIBaseScaffold(
|
||||||
showBackgroundImage: false,
|
showBackgroundImage: false,
|
||||||
title: AppTranslations.kr_purchaseMembership.purchasePackage,
|
title: '套餐选择',
|
||||||
subtitle: '*所有套餐均不限流量不限速度',
|
subtitle: '*所有套餐均不限流量不限速度',
|
||||||
topContentAreaHeight: 110,
|
topContentAreaHeight: 110,
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(bottom: 80.w),
|
padding: EdgeInsets.only(bottom: 80.0),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -67,52 +64,15 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 40.r),
|
margin: EdgeInsets.symmetric(horizontal: 40.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 套餐选择部分
|
// 套餐选择部分
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(0.r),
|
padding: EdgeInsets.all(0.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// if (controller.kr_plans.length > 1)
|
|
||||||
// Container(
|
|
||||||
// height: 32.h,
|
|
||||||
// child: ListView.builder(
|
|
||||||
// scrollDirection: Axis.horizontal,
|
|
||||||
// padding: EdgeInsets.zero,
|
|
||||||
// itemCount: controller.kr_plans.length,
|
|
||||||
// itemBuilder: (context, index) {
|
|
||||||
// final plan = controller.kr_plans[index];
|
|
||||||
// final isSelected = index == controller.kr_selectedPlanIndex.value;
|
|
||||||
// return GestureDetector(
|
|
||||||
// onTap: () => controller.kr_selectPlan(index),
|
|
||||||
// child: Container(
|
|
||||||
// margin: EdgeInsets.only(right: 8.w),
|
|
||||||
// padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
|
|
||||||
// decoration: BoxDecoration(
|
|
||||||
// color: isSelected ? Colors.white : Theme.of(context).cardColor,
|
|
||||||
// borderRadius: BorderRadius.circular(16.r),
|
|
||||||
// border: Border.all(
|
|
||||||
// color: isSelected ? Colors.white : Colors.grey.withOpacity(0.3),
|
|
||||||
// width: 1,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// child: Center(
|
|
||||||
// child: Text(
|
|
||||||
// plan.kr_name,
|
|
||||||
// style: KrAppTextStyle(
|
|
||||||
// fontSize: 13,
|
|
||||||
// color: isSelected ? Colors.white : Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
Column(
|
Column(
|
||||||
// 使用 List.generate 动态创建卡片列表
|
// 使用 List.generate 动态创建卡片列表
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
@@ -130,12 +90,9 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
// 使用 Padding 来为每个卡片添加底部的间距,模拟 mainAxisSpacing
|
// 使用 Padding 来为每个卡片添加底部的间距,模拟 mainAxisSpacing
|
||||||
return Padding(
|
return Padding(
|
||||||
padding:
|
padding:
|
||||||
EdgeInsets.only(bottom: 8.h),
|
EdgeInsets.only(bottom: 8.0),
|
||||||
// 👇 核心改动:父节点 SizedBox 决定了卡片的高度
|
child: SizedBox(
|
||||||
child: ConstrainedBox(
|
height: 140.0,
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: 100.h, // 设置最小高度为 100.h
|
|
||||||
),
|
|
||||||
child: _kr_buildPlanOptionCard(
|
child: _kr_buildPlanOptionCard(
|
||||||
plan,
|
plan,
|
||||||
controller
|
controller
|
||||||
@@ -152,394 +109,15 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
/* SizedBox(height: 16.h),
|
|
||||||
// 套餐描述部分
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(16.r),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_purchaseMembership.packageDescription,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
Obx(() {
|
|
||||||
final featureLabels = controller.kr_getSelectedPlanFeatureLabels();
|
|
||||||
final features = controller.kr_getSelectedPlanFeatures();
|
|
||||||
final isExpanded = controller.kr_isDescriptionExpanded.value;
|
|
||||||
final selectedPlan = controller.kr_plans[controller.kr_selectedPlanIndex.value];
|
|
||||||
|
|
||||||
// 添加流量和设备限制信息
|
|
||||||
final trafficAndDeviceInfo = Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 8.h),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 12.w,
|
|
||||||
vertical: 8.h,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'${AppTranslations.kr_purchaseMembership.trafficLimit}:${controller.kr_getTrafficLimitText(selectedPlan)}',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 4.h),
|
|
||||||
Text(
|
|
||||||
'${AppTranslations.kr_purchaseMembership.deviceLimit}:${controller.kr_getDeviceLimitText(selectedPlan)}',
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// if (featureLabels.isEmpty) {
|
|
||||||
// return Column(
|
|
||||||
// children: [
|
|
||||||
// trafficAndDeviceInfo,
|
|
||||||
// Center(
|
|
||||||
// child: Padding(
|
|
||||||
// padding: EdgeInsets.symmetric(vertical: 16.h),
|
|
||||||
// child: Text(
|
|
||||||
// AppTranslations.kr_purchaseMembership.noData,
|
|
||||||
// style: KrAppTextStyle(
|
|
||||||
// fontSize: 14,
|
|
||||||
// color: Theme.of(context).textTheme.bodySmall?.color,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
final displayCount = isExpanded ? featureLabels.length : (featureLabels.length > 3 ? 3 : featureLabels.length);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
trafficAndDeviceInfo,
|
|
||||||
...List.generate(displayCount, (index) {
|
|
||||||
final feature = features[index];
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 8.h),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Get.dialog(
|
|
||||||
Dialog(
|
|
||||||
backgroundColor: Theme.of(Get.context!).cardColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16.r),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
feature.kr_label,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8.h),
|
|
||||||
if (feature.kr_details.isNotEmpty)
|
|
||||||
...feature.kr_details.map((detail) => Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 8.h),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (detail.kr_label.isNotEmpty) ...[
|
|
||||||
Text(
|
|
||||||
detail.kr_label,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 4.h),
|
|
||||||
],
|
|
||||||
Text(
|
|
||||||
detail.kr_description,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)).toList(),
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_dialog.kr_ok,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 12.w,
|
|
||||||
vertical: 8.h,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(Get.context!).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
featureLabels[index],
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(4.r),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.info_outline,
|
|
||||||
size: 16.r,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
if (featureLabels.length > 3)
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => controller.kr_toggleDescriptionExpanded(),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4.h),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
isExpanded
|
|
||||||
? AppTranslations.kr_purchaseMembership.collapse
|
|
||||||
: AppTranslations.kr_purchaseMembership.expand,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
Icon(
|
|
||||||
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
|
|
||||||
size: 16.r,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
// 支付方式选择部分
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(16.r),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppTranslations.kr_purchaseMembership.paymentMethod,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
Obx(() => ListView.separated(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: controller.kr_paymentMethods.length,
|
|
||||||
separatorBuilder: (context, index) => Divider(
|
|
||||||
height: 1.w,
|
|
||||||
indent: 44.w,
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final paymentMethod = controller.kr_paymentMethods[index];
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => controller.kr_selectPaymentMethod(index),
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 12.r, horizontal: 16.r),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 32.w,
|
|
||||||
height: 32.w,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: paymentMethod.icon.isNotEmpty
|
|
||||||
? KRNetworkImage(
|
|
||||||
kr_imageUrl: paymentMethod.icon,
|
|
||||||
kr_width: 20.w,
|
|
||||||
kr_height: 20.w,
|
|
||||||
kr_placeholder: SizedBox(
|
|
||||||
width: 20.w,
|
|
||||||
height: 20.w,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
kr_errorWidget: Icon(
|
|
||||||
Icons.payment_rounded,
|
|
||||||
size: 20.w,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
Icons.payment_rounded,
|
|
||||||
size: 20.w,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12.w),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
controller.kr_getPaymentMethodTitle(paymentMethod),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
final isSelected = index == controller.kr_selectedPaymentMethodIndex.value;
|
|
||||||
return Container(
|
|
||||||
width: 24.w,
|
|
||||||
height: 24.w,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? Colors.white : Colors.transparent,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected ? Colors.white : Colors.grey.withOpacity(0.3),
|
|
||||||
width: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: isSelected
|
|
||||||
? Icon(
|
|
||||||
Icons.check,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16.r,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),*/
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 160.w, // 距离顶部的距离
|
top: 160.0, // 距离顶部的距离
|
||||||
right: 10.w, // 固定在右侧 20.w 的位置
|
right: 10.0, // 固定在右侧 20.w 的位置
|
||||||
child: KrLocalImage(
|
child: KrLocalImage(
|
||||||
imageName: 'purchase_slogan',
|
imageName: 'purchase_slogan',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
@@ -589,7 +167,8 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
int planIndex,
|
int planIndex,
|
||||||
int? discountIndex,
|
int? discountIndex,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
bool isFirst,) {
|
bool isFirst,
|
||||||
|
) {
|
||||||
// 由于移除了 Obx,isSelected 的值在构建时就固定了。
|
// 由于移除了 Obx,isSelected 的值在构建时就固定了。
|
||||||
// 它将不再响应用户的点击事件来改变UI。
|
// 它将不再响应用户的点击事件来改变UI。
|
||||||
// 根据您的要求,isSelected 的值与 isFirst 相反。
|
// 根据您的要求,isSelected 的值与 isFirst 相反。
|
||||||
@@ -606,25 +185,27 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
padding: EdgeInsets.all(5), // 边框宽度
|
padding: EdgeInsets.all(5), // 边框宽度
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black, // 边框颜色
|
color: Colors.black, // 边框颜色
|
||||||
borderRadius: BorderRadius.circular(43.r), // 外部圆角 = 内部圆角 + 边框宽度
|
borderRadius: BorderRadius.circular(43.0), // 外部圆角 = 内部圆角 + 边框宽度
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(38.r), // 内部圆角
|
borderRadius: BorderRadius.circular(38.0), // 内部圆角
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.fromLTRB(45.w, 10.w, 0.w, 10.w),
|
padding: EdgeInsets.fromLTRB(45.0, 10.0, 0.0, 10.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? Colors.black : Theme.of(context).primaryColor,
|
color: isSelected ? Colors.black : Theme.of(context).primaryColor,
|
||||||
borderRadius: BorderRadius.circular(38.r),
|
borderRadius: BorderRadius.circular(38.0),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Column( mainAxisSize: MainAxisSize.min,
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
controller.kr_getTimeStr(plan, discountIndex: discountIndex),
|
controller.kr_getTimeStr(plan,
|
||||||
|
discountIndex: discountIndex),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -642,8 +223,7 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
color: isSelected
|
color: isSelected
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: Theme.of(context).textTheme.bodyMedium?.color,
|
: Theme.of(context).textTheme.bodyMedium?.color,
|
||||||
)
|
)),
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
'约¥${controller.kr_getDayPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}/天',
|
'约¥${controller.kr_getDayPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}/天',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -652,11 +232,8 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
color: isSelected
|
color: isSelected
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: Theme.of(context).textTheme.bodyMedium?.color,
|
: Theme.of(context).textTheme.bodyMedium?.color,
|
||||||
)
|
)),
|
||||||
),
|
]),
|
||||||
]
|
|
||||||
),
|
|
||||||
|
|
||||||
if (!isFirst)
|
if (!isFirst)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -665,9 +242,9 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
angle: 0.785398, // 45度角 (π/4)
|
angle: 0.785398, // 45度角 (π/4)
|
||||||
child: Container(
|
child: Container(
|
||||||
// 调整位置,让标签的一部分显示在容器外
|
// 调整位置,让标签的一部分显示在容器外
|
||||||
transform: Matrix4.translationValues(19.w, -12.w, 0),
|
transform: Matrix4.translationValues(30, -10, 0),
|
||||||
width: 92.w, // 适当的宽度
|
width: 130, // 适当的宽度
|
||||||
height: 16.w, // 固定高度
|
height: 20, // 固定高度
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).primaryColor, // 使用主题色作为背景
|
color: Theme.of(context).primaryColor, // 使用主题色作为背景
|
||||||
),
|
),
|
||||||
@@ -676,7 +253,7 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
'${controller.kr_getDiscountText(plan, discountIndex!)} OFF',
|
'${controller.kr_getDiscountText(plan, discountIndex!)} OFF',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black, // 白色文字更清晰
|
color: Colors.black, // 白色文字更清晰
|
||||||
fontSize: 14.sp,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -685,47 +262,6 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/* if (discountIndex != null && plan.kr_discount.isNotEmpty) ...[
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 8.w,
|
|
||||||
vertical: 2.h
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: plan.kr_discount[discountIndex].kr_discount < 100
|
|
||||||
? Colors.red.withOpacity(0.08)
|
|
||||||
: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(4.r),
|
|
||||||
border: plan.kr_discount[discountIndex].kr_discount < 100
|
|
||||||
? Border.all(
|
|
||||||
color: Colors.red.withOpacity(0.2),
|
|
||||||
width: 1,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
boxShadow: plan.kr_discount[discountIndex].kr_discount < 100
|
|
||||||
? [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.red.withOpacity(0.05),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
spreadRadius: 0,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
controller.kr_getDiscountText(plan, discountIndex),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: plan.kr_discount[discountIndex].kr_discount < 100
|
|
||||||
? Colors.red.withOpacity(0.9)
|
|
||||||
: Colors.transparent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],*/
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -733,56 +269,4 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部部分
|
|
||||||
Widget _kr_buildBottomSection(BuildContext context) {
|
|
||||||
// 如果正在加载或没有数据,不显示底部按钮
|
|
||||||
if (controller.kr_isLoading.value || controller.kr_plans.isEmpty || controller.kr_paymentMethods.isEmpty) {
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.all(16.r),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
offset: Offset(0, -2),
|
|
||||||
blurRadius: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
KRDialog.show(
|
|
||||||
title: AppTranslations.kr_purchaseMembership.confirmPurchase,
|
|
||||||
message: AppTranslations.kr_purchaseMembership.confirmPurchaseDesc,
|
|
||||||
cancelText: AppTranslations.kr_dialog.kr_cancel,
|
|
||||||
confirmText: AppTranslations.kr_dialog.kr_confirm,
|
|
||||||
onConfirm: () => controller.kr_startSubscription(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8.r),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
controller.kr_getSubscribeButtonText(),
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,13 +170,16 @@ class KRSplashController extends GetxController {
|
|||||||
print('📞 准备调用 KRSiteConfigService().initialize()...');
|
print('📞 准备调用 KRSiteConfigService().initialize()...');
|
||||||
final success = await KRSiteConfigService().initialize();
|
final success = await KRSiteConfigService().initialize();
|
||||||
final crispId = await KRSiteConfigService().getCrispId();
|
final crispId = await KRSiteConfigService().getCrispId();
|
||||||
|
final device_limit = await KRSiteConfigService().getDeviceLimit();
|
||||||
print('📞 KRSiteConfigService().initialize() 返回: $success');
|
print('📞 KRSiteConfigService().initialize() 返回: $success');
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
final config = AppConfig.getInstance();
|
final config = AppConfig.getInstance();
|
||||||
config.kr_website_id = crispId;
|
config.kr_website_id = crispId;
|
||||||
|
config.device_limit = device_limit;
|
||||||
print('📞 KRSiteConfigService().initialize() 返回: $crispId');
|
print('📞 KRSiteConfigService().initialize() 返回: $crispId');
|
||||||
print('AppConfig website_id 已更新为: ${config.kr_website_id}');
|
print('AppConfig website_id 已更新为: ${config.kr_website_id}');
|
||||||
|
print('AppConfig device_limit 已更新为: ${config.device_limit}');
|
||||||
|
|
||||||
// print('[SPLASH_TIMING] ✅ 网站配置初始化成功');
|
// print('[SPLASH_TIMING] ✅ 网站配置初始化成功');
|
||||||
// await _kr_checkAndPerformDeviceLogin();
|
// await _kr_checkAndPerformDeviceLogin();
|
||||||
@@ -285,7 +288,7 @@ class KRSplashController extends GetxController {
|
|||||||
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
||||||
print('⏱️ 初始化超时,直接跳转到主页');
|
print('⏱️ 初始化超时,直接跳转到主页');
|
||||||
// 超时后直接跳转到主页,让用户可以手动重试
|
// 超时后直接跳转到主页,让用户可以手动重试
|
||||||
// Get.offAllNamed(Routes.KR_MAIN);
|
// Get.offAllNamed(Routes.KR_HOME);
|
||||||
HIDialog.show(
|
HIDialog.show(
|
||||||
message: '初始化超时,请检查网络或重试',
|
message: '初始化超时,请检查网络或重试',
|
||||||
confirmText: '重试',
|
confirmText: '重试',
|
||||||
@@ -484,7 +487,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
||||||
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +496,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
||||||
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -582,7 +585,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
||||||
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +729,7 @@ class KRSplashController extends GetxController {
|
|||||||
kr_isLoading.value = false;
|
kr_isLoading.value = false;
|
||||||
|
|
||||||
// 直接跳转到主页
|
// 直接跳转到主页
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ class HttpUtil {
|
|||||||
void updateBaseUrl() {
|
void updateBaseUrl() {
|
||||||
String newBaseUrl = AppConfig.getInstance().baseUrl;
|
String newBaseUrl = AppConfig.getInstance().baseUrl;
|
||||||
if (_dio.options.baseUrl != newBaseUrl) {
|
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;
|
_dio.options.baseUrl = newBaseUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,7 +241,9 @@ class HttpUtil {
|
|||||||
return null;
|
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('🔍 请求头: $headers', tag: 'HttpUtil');
|
||||||
KRLogUtil.kr_i('🔍 请求userId: $userId', tag: 'HttpUtil');
|
KRLogUtil.kr_i('🔍 请求userId: $userId', tag: 'HttpUtil');
|
||||||
@@ -326,18 +329,22 @@ class HttpUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BaseResponse<T>.fromJson({
|
if (err.type == DioExceptionType.unknown) {
|
||||||
'code': code,
|
final _pathOnly = (err.requestOptions.path.isNotEmpty)
|
||||||
'msg': msg,
|
? err.requestOptions.path
|
||||||
'data': <String, dynamic>{}
|
: 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) {
|
} catch (e) {
|
||||||
if (isShowLoading) {
|
if (isShowLoading) {
|
||||||
KRCommonUtil.kr_hideLoading();
|
KRCommonUtil.kr_hideLoading();
|
||||||
}
|
}
|
||||||
return BaseResponse<T>.fromJson({
|
return BaseResponse<T>.fromJson({
|
||||||
'code': -90000,
|
'code': -90000,
|
||||||
'msg': e.toString(),
|
'msg': '${e.toString()} (${path})',
|
||||||
'data': <String, dynamic>{}
|
'data': <String, dynamic>{}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -362,7 +369,8 @@ class MyInterceptor extends Interceptor {
|
|||||||
@override
|
@override
|
||||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||||
if (kDebugMode) {
|
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 (response.data != null) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
@@ -375,7 +383,8 @@ class MyInterceptor extends Interceptor {
|
|||||||
@override
|
@override
|
||||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
print(
|
||||||
|
'<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||||||
}
|
}
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('Error Type: ${err.type}');
|
print('Error Type: ${err.type}');
|
||||||
@@ -396,6 +405,8 @@ class MyInterceptor extends Interceptor {
|
|||||||
|
|
||||||
/// 自定义简洁 HTTP 拦截器(无边框符号)
|
/// 自定义简洁 HTTP 拦截器(无边框符号)
|
||||||
class _KRSimpleHttpInterceptor extends Interceptor {
|
class _KRSimpleHttpInterceptor extends Interceptor {
|
||||||
|
static String? _lastPath;
|
||||||
|
static int _lastTsMs = 0;
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
@@ -454,7 +465,8 @@ class _KRSimpleHttpInterceptor extends Interceptor {
|
|||||||
@override
|
@override
|
||||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||||
if (kDebugMode) {
|
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 (response.data != null) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
@@ -514,7 +526,8 @@ class _KRSimpleHttpInterceptor extends Interceptor {
|
|||||||
@override
|
@override
|
||||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
print(
|
||||||
|
'<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||||||
}
|
}
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('Error Type: ${err.type}');
|
print('Error Type: ${err.type}');
|
||||||
@@ -529,6 +542,19 @@ class _KRSimpleHttpInterceptor extends Interceptor {
|
|||||||
print('Response Data: ${err.response?.data}');
|
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);
|
handler.next(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ class SlideTransparentTransition extends CustomTransition {
|
|||||||
|
|
||||||
SlideTransparentTransition({
|
SlideTransparentTransition({
|
||||||
this.direction = SlideDirection.rightToLeft,
|
this.direction = SlideDirection.rightToLeft,
|
||||||
this.duration = const Duration(milliseconds: 4000),
|
this.duration = const Duration(milliseconds: 2000),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// 接口名称
|
/// 接口名称
|
||||||
abstract class Api {
|
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";
|
static const String kr_checkSubscription = "/v1/public/user/subscribe_status";
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class KRAuthApi {
|
|||||||
|
|
||||||
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
|
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
|
||||||
.request<KRIsRegister>(Api.kr_isRegister, data,
|
.request<KRIsRegister>(Api.kr_isRegister, data,
|
||||||
method: HttpMethod.POST, isShowLoading: true);
|
method: HttpMethod.GET, isShowLoading: true);
|
||||||
|
|
||||||
if (!baseResponse.isSuccess) {
|
if (!baseResponse.isSuccess) {
|
||||||
return left(
|
return left(
|
||||||
@@ -298,7 +298,7 @@ class KRAuthApi {
|
|||||||
|
|
||||||
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
|
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
|
||||||
.request<dynamic>(Api.kr_deleteAccount, data,
|
.request<dynamic>(Api.kr_deleteAccount, data,
|
||||||
method: HttpMethod.DELETE, isShowLoading: true);
|
method: HttpMethod.POST, isShowLoading: true);
|
||||||
if (!baseResponse.isSuccess) {
|
if (!baseResponse.isSuccess) {
|
||||||
return left(
|
return left(
|
||||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||||
|
|||||||
@@ -341,6 +341,11 @@ class KRSiteConfigService extends ChangeNotifier {
|
|||||||
return _siteConfig?.site.crispId ?? '0';
|
return _siteConfig?.site.crispId ?? '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取设备限制
|
||||||
|
String getDeviceLimit() {
|
||||||
|
return _siteConfig?.site.deviceLimit ?? '0';
|
||||||
|
}
|
||||||
|
|
||||||
/// 重置配置
|
/// 重置配置
|
||||||
void reset() {
|
void reset() {
|
||||||
_siteConfig = null;
|
_siteConfig = null;
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ class KRSubscribeService {
|
|||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
||||||
|
|
||||||
// 更新试用和订阅状态
|
// 更新试用和订阅状态
|
||||||
_kr_updateSubscribeStatus();
|
_kr_updateSubscribeStatus();
|
||||||
|
|
||||||
@@ -722,6 +723,7 @@ class KRSubscribeService {
|
|||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
KRSingBoxImp.instance.kr_saveOutbounds([]);
|
KRSingBoxImp.instance.kr_saveOutbounds([]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前订阅
|
/// 获取当前订阅
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import '../../../singbox/model/singbox_status.dart';
|
|||||||
import '../../utils/kr_country_util.dart';
|
import '../../utils/kr_country_util.dart';
|
||||||
import '../../utils/kr_log_util.dart';
|
import '../../utils/kr_log_util.dart';
|
||||||
import '../../utils/kr_secure_storage.dart';
|
import '../../utils/kr_secure_storage.dart';
|
||||||
|
import '../../utils/kr_windows_dns_util.dart';
|
||||||
import '../../common/app_run_data.dart';
|
import '../../common/app_run_data.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
@@ -98,6 +99,9 @@ class KRSingBoxImp {
|
|||||||
/// 活动的出站分组
|
/// 活动的出站分组
|
||||||
RxList<SingboxOutboundGroup> kr_activeGroups = <SingboxOutboundGroup>[].obs;
|
RxList<SingboxOutboundGroup> kr_activeGroups = <SingboxOutboundGroup>[].obs;
|
||||||
|
|
||||||
|
// 🔒 Windows DNS 优化:标志位,记录 DNS 是否已备份
|
||||||
|
bool _dnsBackedUp = false;
|
||||||
|
|
||||||
/// 所有的出站分组
|
/// 所有的出站分组
|
||||||
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
|
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
|
||||||
|
|
||||||
@@ -107,6 +111,9 @@ class KRSingBoxImp {
|
|||||||
/// 初始化标志,防止重复初始化
|
/// 初始化标志,防止重复初始化
|
||||||
bool _kr_isInitialized = false;
|
bool _kr_isInitialized = false;
|
||||||
|
|
||||||
|
/// 初始化进行中共享 Future(single-flight 防并发)
|
||||||
|
Future<void>? _kr_initFuture;
|
||||||
|
|
||||||
/// 当前混合代理端口是否就绪
|
/// 当前混合代理端口是否就绪
|
||||||
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
|
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
|
||||||
|
|
||||||
@@ -139,12 +146,23 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
/// 初始化
|
/// 初始化
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
// 防止重复初始化
|
// 防止重复初始化(已完成)
|
||||||
if (_kr_isInitialized) {
|
if (_kr_isInitialized) {
|
||||||
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
|
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 防止并发重复初始化(进行中复用同一个 Future)
|
||||||
|
if (_kr_initFuture != null) {
|
||||||
|
KRLogUtil.kr_i('SingBox 初始化进行中,等待完成(single-flight)', tag: 'SingBox');
|
||||||
|
await _kr_initFuture;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立 single-flight 共享 Future
|
||||||
|
final completer = Completer<void>();
|
||||||
|
_kr_initFuture = completer.future;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
KRLogUtil.kr_i('开始初始化 SingBox');
|
KRLogUtil.kr_i('开始初始化 SingBox');
|
||||||
// 在应用启动时初始化
|
// 在应用启动时初始化
|
||||||
@@ -410,12 +428,22 @@ class KRSingBoxImp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ SingBox 初始化完成');
|
KRLogUtil.kr_i('✅ SingBox 初始化完成');
|
||||||
|
|
||||||
|
// 🔧 提取 geosite 文件到 workingDir
|
||||||
|
KRLogUtil.kr_i('📦 开始提取 geosite 文件...', tag: 'SingBox');
|
||||||
|
await _kr_extractGeositeFiles();
|
||||||
|
KRLogUtil.kr_i('✅ geosite 文件提取完成', tag: 'SingBox');
|
||||||
|
|
||||||
_kr_isInitialized = true;
|
_kr_isInitialized = true;
|
||||||
|
|
||||||
// 🔑 关键:在初始化完成后立即订阅状态变化流
|
// 🔑 关键:在初始化完成后立即订阅状态变化流
|
||||||
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
||||||
_kr_subscribeToStatus();
|
_kr_subscribeToStatus();
|
||||||
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 完成 single-flight
|
||||||
|
completer.complete();
|
||||||
|
_kr_initFuture = null;
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
||||||
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
||||||
@@ -451,26 +479,87 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// 如果初始化失败,允许下次重试
|
// 如果初始化失败,允许下次重试
|
||||||
_kr_isInitialized = false;
|
_kr_isInitialized = false;
|
||||||
|
// 失败时通知等待者并清理 single-flight
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(e, stackTrace);
|
||||||
|
}
|
||||||
|
_kr_initFuture = null;
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 提取 geosite 文件到 workingDir
|
||||||
|
Future<void> _kr_extractGeositeFiles() async {
|
||||||
|
try {
|
||||||
|
// 创建 geosite 目录
|
||||||
|
final geositeDir = Directory(p.join(kr_configDics.workingDir.path, 'geosite'));
|
||||||
|
if (!geositeDir.existsSync()) {
|
||||||
|
await geositeDir.create(recursive: true);
|
||||||
|
KRLogUtil.kr_i('✅ 已创建 geosite 目录: ${geositeDir.path}', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要提取的文件列表
|
||||||
|
final files = ['geoip-cn.srs', 'geosite-cn.srs'];
|
||||||
|
|
||||||
|
for (final filename in files) {
|
||||||
|
final assetPath = 'assets/geosite/$filename';
|
||||||
|
final targetPath = p.join(geositeDir.path, filename);
|
||||||
|
final targetFile = File(targetPath);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
if (targetFile.existsSync()) {
|
||||||
|
final fileSize = await targetFile.length();
|
||||||
|
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 assets 加载文件
|
||||||
|
KRLogUtil.kr_i('📥 正在提取 $filename...', tag: 'SingBox');
|
||||||
|
final byteData = await rootBundle.load(assetPath);
|
||||||
|
final bytes = byteData.buffer.asUint8List();
|
||||||
|
|
||||||
|
// 写入文件系统
|
||||||
|
await targetFile.writeAsBytes(bytes);
|
||||||
|
final writtenSize = await targetFile.length();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
KRLogUtil.kr_e('❌ 提取 geosite 文件失败: $e', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_e('堆栈: $stackTrace', tag: 'SingBox');
|
||||||
|
// 不抛出异常,让应用继续运行(使用远程规则集作为后备)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _getConfigOption() {
|
Map<String, dynamic> _getConfigOption() {
|
||||||
// 不使用缓存,每次都重新生成配置
|
// 不使用缓存,每次都重新生成配置
|
||||||
// if (kr_configOption.isNotEmpty) {
|
// if (kr_configOption.isNotEmpty) {
|
||||||
// return kr_configOption;
|
// return kr_configOption;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// 🔧 关键修复:全局代理模式下,region 强制设为 'other',libcore 就不会生成国家直连规则
|
||||||
|
final String effectiveRegion;
|
||||||
|
if (kr_connectionType.value == KRConnectionType.global) {
|
||||||
|
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
|
||||||
|
KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other,所有流量走代理', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
|
||||||
|
KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
final op = {
|
final op = {
|
||||||
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
|
||||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||||
"use-xray-core-when-possible": false,
|
"use-xray-core-when-possible": false,
|
||||||
"execute-config-as-is": false,
|
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
||||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||||
"resolve-destination": false,
|
"resolve-destination": false,
|
||||||
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
||||||
"remote-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 使用 Cloudflare DNS
|
"remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
|
||||||
"remote-dns-domain-strategy": "prefer_ipv4",
|
"remote-dns-domain-strategy": "prefer_ipv4",
|
||||||
"direct-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 统一使用 1.1.1.1
|
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
|
||||||
"direct-dns-domain-strategy": "prefer_ipv4",
|
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||||
"mixed-port": kr_port,
|
"mixed-port": kr_port,
|
||||||
"tproxy-port": kr_port,
|
"tproxy-port": kr_port,
|
||||||
@@ -491,7 +580,7 @@ class KRSingBoxImp {
|
|||||||
"enable-fake-dns": false,
|
"enable-fake-dns": false,
|
||||||
"enable-dns-routing": true,
|
"enable-dns-routing": true,
|
||||||
"independent-dns-cache": true,
|
"independent-dns-cache": true,
|
||||||
"rules": [],
|
"rules": _kr_buildHiddifyRules(),
|
||||||
"mux": {
|
"mux": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"padding": false,
|
"padding": false,
|
||||||
@@ -536,9 +625,65 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
kr_configOption = op;
|
kr_configOption = op;
|
||||||
|
|
||||||
|
// 🔧 调试日志:确认自定义规则已添加
|
||||||
|
final rules = op["rules"] as List?;
|
||||||
|
KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', tag: 'SingBox');
|
||||||
|
if (rules != null && rules.isNotEmpty) {
|
||||||
|
for (var rule in rules) {
|
||||||
|
final ruleMap = rule as Map<String, dynamic>;
|
||||||
|
KRLogUtil.kr_i(' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _kr_buildHiddifyRules() {
|
||||||
|
final rules = <Map<String, dynamic>>[];
|
||||||
|
rules.add({
|
||||||
|
"domains": "domain:api.hifast.biz",
|
||||||
|
"outbound": "bypass"
|
||||||
|
});
|
||||||
|
|
||||||
|
final nodeDomains = _kr_collectNodeDomains();
|
||||||
|
for (final d in nodeDomains) {
|
||||||
|
rules.add({
|
||||||
|
"domains": "domain:$d",
|
||||||
|
"outbound": "bypass"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ 节点域名白名单数量: ${nodeDomains.length}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}', tag: 'SingBox');
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> _kr_collectNodeDomains() {
|
||||||
|
final set = <String>{};
|
||||||
|
|
||||||
|
void addFromOutbound(Map<String, dynamic> o) {
|
||||||
|
final server = o['server']?.toString();
|
||||||
|
if (server != null && server.isNotEmpty && InternetAddress.tryParse(server) == null) {
|
||||||
|
set.add(server.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
final tls = o['tls'];
|
||||||
|
if (tls is Map) {
|
||||||
|
final sni = tls['server_name']?.toString();
|
||||||
|
if (sni != null && sni.isNotEmpty && InternetAddress.tryParse(sni) == null) {
|
||||||
|
set.add(sni.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final g in kr_outbounds) {
|
||||||
|
addFromOutbound(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
/// 订阅状态变化流
|
/// 订阅状态变化流
|
||||||
/// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI
|
/// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI
|
||||||
void _kr_subscribeToStatus() {
|
void _kr_subscribeToStatus() {
|
||||||
@@ -1010,6 +1155,9 @@ class KRSingBoxImp {
|
|||||||
if (outbound['method'] != null) {
|
if (outbound['method'] != null) {
|
||||||
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
|
if (outbound['interval'] != null) {
|
||||||
|
KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox');
|
||||||
|
}
|
||||||
if (outbound['password'] != null) {
|
if (outbound['password'] != null) {
|
||||||
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
@@ -1031,17 +1179,80 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
||||||
|
|
||||||
// 只保存 outbounds,Mobile.buildConfig() 会添加其他配置
|
// 🔧 修复:生成完整的 SingBox 配置
|
||||||
final map = {
|
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
|
||||||
"outbounds": kr_outbounds
|
// 现在保存完整配置,包含所有必需字段
|
||||||
|
final Map<String, dynamic> fullConfig = {
|
||||||
|
"log": {
|
||||||
|
"level": "debug",
|
||||||
|
"timestamp": true
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"tag": "dns-remote",
|
||||||
|
"address": "https://1.1.1.1/dns-query",
|
||||||
|
"address_resolver": "dns-direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "dns-direct",
|
||||||
|
"address": "local",
|
||||||
|
"detour": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
|
||||||
|
"final": "dns-remote",
|
||||||
|
"strategy": "prefer_ipv4"
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"tag": "tun-in",
|
||||||
|
"interface_name": "utun",
|
||||||
|
"inet4_address": "172.19.0.1/30",
|
||||||
|
"auto_route": true,
|
||||||
|
"strict_route": true,
|
||||||
|
"sniff": true,
|
||||||
|
"sniff_override_destination": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
// 🔧 修复:添加 selector 组,让用户可以手动选择节点
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "proxy",
|
||||||
|
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
|
||||||
|
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
|
||||||
|
},
|
||||||
|
...kr_outbounds,
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "block",
|
||||||
|
"tag": "block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"tag": "dns-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
|
||||||
|
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
|
||||||
|
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
final file = _file(kr_configName);
|
final file = _file(kr_configName);
|
||||||
final temp = _tempFile(kr_configName);
|
final temp = _tempFile(kr_configName);
|
||||||
final mapStr = jsonEncode(map);
|
final mapStr = jsonEncode(fullConfig);
|
||||||
|
|
||||||
KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 完整配置文件内容: $mapStr', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
||||||
|
|
||||||
await file.writeAsString(mapStr);
|
await file.writeAsString(mapStr);
|
||||||
await temp.writeAsString(mapStr);
|
await temp.writeAsString(mapStr);
|
||||||
@@ -1058,6 +1269,128 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构建 DNS 规则
|
||||||
|
List<Map<String, dynamic>> _kr_buildDnsRules() {
|
||||||
|
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
|
final rules = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
|
||||||
|
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
|
||||||
|
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||||
|
rules.add({
|
||||||
|
"rule_set": [
|
||||||
|
"geoip-$currentCountryCode",
|
||||||
|
"geosite-$currentCountryCode",
|
||||||
|
],
|
||||||
|
"server": "dns-direct"
|
||||||
|
});
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox');
|
||||||
|
} else if (kr_connectionType.value == KRConnectionType.global) {
|
||||||
|
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则,所有DNS查询走代理', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建路由规则
|
||||||
|
List<Map<String, dynamic>> _kr_buildRouteRules() {
|
||||||
|
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
|
final rules = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
// 基础规则: DNS 查询走 dns-out
|
||||||
|
rules.add({
|
||||||
|
"protocol": "dns",
|
||||||
|
"outbound": "dns-out"
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 自定义域名直连规则(优先级高,放在前面)
|
||||||
|
// rules.add({
|
||||||
|
// "domain_suffix": ["ip138.com"],
|
||||||
|
// "outbound": "direct"
|
||||||
|
// });
|
||||||
|
|
||||||
|
// KRLogUtil.kr_i('✅ 添加自定义域名直连规则: ip138.com -> direct', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
|
||||||
|
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
|
||||||
|
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||||
|
rules.add({
|
||||||
|
"rule_set": [
|
||||||
|
"geoip-$currentCountryCode",
|
||||||
|
"geosite-$currentCountryCode",
|
||||||
|
],
|
||||||
|
"outbound": "direct"
|
||||||
|
});
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox');
|
||||||
|
} else if (kr_connectionType.value == KRConnectionType.global) {
|
||||||
|
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建规则集配置
|
||||||
|
List<Map<String, dynamic>> _kr_buildRuleSets() {
|
||||||
|
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
|
final ruleSets = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
// 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集
|
||||||
|
// 如果是"全局代理"模式,即使选择了国家也不加载规则集
|
||||||
|
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||||
|
// 检查本地文件是否存在
|
||||||
|
final geoipFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geoip-$currentCountryCode.srs'));
|
||||||
|
final geositeFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geosite-$currentCountryCode.srs'));
|
||||||
|
|
||||||
|
if (geoipFile.existsSync() && geositeFile.existsSync()) {
|
||||||
|
// ✅ 使用本地文件
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "local",
|
||||||
|
"tag": "geoip-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "local",
|
||||||
|
"tag": "geosite-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"path": "./geosite/geosite-$currentCountryCode.srs"
|
||||||
|
});
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ 使用本地规则集: $currentCountryCode', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i(' - geoip: ./geosite/geoip-$currentCountryCode.srs', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i(' - geosite: ./geosite/geosite-$currentCountryCode.srs', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
// ❌ 本地文件不存在,使用远程规则集作为后备
|
||||||
|
KRLogUtil.kr_w('⚠️ 本地规则集不存在,使用远程规则集', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_w(' - geoip 文件存在: ${geoipFile.existsSync()}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_w(' - geosite 文件存在: ${geositeFile.existsSync()}', tag: 'SingBox');
|
||||||
|
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "remote",
|
||||||
|
"tag": "geoip-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs",
|
||||||
|
"download_detour": "direct",
|
||||||
|
"update_interval": "7d"
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "remote",
|
||||||
|
"tag": "geosite-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
|
||||||
|
"download_detour": "direct",
|
||||||
|
"update_interval": "7d"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ruleSets;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> kr_start() async {
|
Future<void> kr_start() async {
|
||||||
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
||||||
try {
|
try {
|
||||||
@@ -1069,9 +1402,36 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
||||||
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
|
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 🔧 强制重新生成配置文件(确保最新的路由规则生效)
|
||||||
|
if (kr_outbounds.isNotEmpty) {
|
||||||
|
KRLogUtil.kr_i('🔄 启动前强制重新生成配置文件...', tag: 'SingBox');
|
||||||
|
kr_saveOutbounds(kr_outbounds);
|
||||||
|
// 等待配置文件写入完成
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
|
||||||
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
|
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 🔑 Windows 平台:仅在首次启动时备份 DNS 设置(优化:避免重复备份)
|
||||||
|
if (Platform.isWindows && !_dnsBackedUp) {
|
||||||
|
KRLogUtil.kr_i('🪟 Windows 平台,首次启动备份 DNS 设置...', tag: 'SingBox');
|
||||||
|
try {
|
||||||
|
final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
|
||||||
|
if (backupSuccess) {
|
||||||
|
_dnsBackedUp = true; // 标记已备份
|
||||||
|
KRLogUtil.kr_i('✅ Windows DNS 备份成功', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_w('⚠️ Windows DNS 备份失败,将在停止时使用兜底恢复', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_w('⚠️ Windows DNS 备份异常: $e,将在停止时使用兜底恢复', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
} else if (Platform.isWindows && _dnsBackedUp) {
|
||||||
|
KRLogUtil.kr_i('⏭️ Windows 平台,DNS 已备份,跳过重复备份(节点切换优化)', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
|
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
|
||||||
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
|
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
|
||||||
try {
|
try {
|
||||||
@@ -1225,12 +1585,12 @@ class KRSingBoxImp {
|
|||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// 添加超时保护,防止 stop() 调用阻塞
|
// 添加超时保护,防止 stop() 调用阻塞
|
||||||
// 缩短超时时间,避免用户等待过久
|
// 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间
|
||||||
try {
|
try {
|
||||||
await kr_singBox.stop().run().timeout(
|
await kr_singBox.stop().run().timeout(
|
||||||
const Duration(seconds: 3),
|
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
|
||||||
onTimeout: () {
|
onTimeout: () {
|
||||||
KRLogUtil.kr_w('⚠️ 停止操作超时(3秒),强制继续', tag: 'SingBox');
|
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
|
||||||
return const Left('timeout');
|
return const Left('timeout');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1239,7 +1599,66 @@ class KRSingBoxImp {
|
|||||||
// 继续执行清理操作
|
// 继续执行清理操作
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔑 Windows 平台:仅在有备份时恢复 DNS 设置
|
||||||
|
if (Platform.isWindows && _dnsBackedUp) {
|
||||||
|
KRLogUtil.kr_i('🪟 Windows 平台,等待 sing-box 完全停止...', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 🔧 P3优化: 监听状态而非固定延迟,确保 sing-box 真正停止后再恢复 DNS
|
||||||
|
try {
|
||||||
|
// 如果当前已经是停止状态,直接继续
|
||||||
|
if (kr_status.value is SingboxStopped) {
|
||||||
|
KRLogUtil.kr_i('✅ sing-box 已经是停止状态,立即恢复 DNS', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
// 等待状态变为停止,最多等待3秒
|
||||||
|
final completer = Completer<void>();
|
||||||
|
late final Worker worker;
|
||||||
|
|
||||||
|
worker = ever(kr_status, (status) {
|
||||||
|
if (status is SingboxStopped) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete();
|
||||||
|
worker.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await completer.future.timeout(
|
||||||
|
const Duration(seconds: 3),
|
||||||
|
onTimeout: () {
|
||||||
|
KRLogUtil.kr_w('⏱️ 等待停止状态超时,继续执行 DNS 恢复', tag: 'SingBox');
|
||||||
|
worker.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ sing-box 已完全停止,开始恢复 DNS...', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 状态监听异常: $e,继续执行 DNS 恢复', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试恢复 DNS
|
||||||
|
final restoreSuccess = await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
|
||||||
|
if (restoreSuccess) {
|
||||||
|
KRLogUtil.kr_i('✅ Windows DNS 恢复成功', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_e('❌ Windows DNS 恢复失败', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ Windows DNS 恢复异常: $e', tag: 'SingBox');
|
||||||
|
// 异常时也会在工具类内部执行兜底恢复
|
||||||
|
} finally {
|
||||||
|
// 🔧 P0修复2: 使用 finally 确保标志位始终被重置,避免下次启动跳过备份
|
||||||
|
_dnsBackedUp = false;
|
||||||
|
KRLogUtil.kr_i('🔄 重置 DNS 备份标志位', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
} else if (Platform.isWindows && !_dnsBackedUp) {
|
||||||
|
KRLogUtil.kr_i('⏭️ Windows 平台,DNS 未备份,跳过恢复', tag: 'SingBox');
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
} else {
|
||||||
|
// 非 Windows 平台正常等待
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
// 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新
|
// 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新
|
||||||
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
|
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
|
||||||
@@ -1261,6 +1680,21 @@ class KRSingBoxImp {
|
|||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
KRLogUtil.kr_e('停止服务时出错: $e');
|
KRLogUtil.kr_e('停止服务时出错: $e');
|
||||||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||||||
|
|
||||||
|
// 🔑 即使出错,也要尝试恢复 Windows DNS
|
||||||
|
if (Platform.isWindows && _dnsBackedUp) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 停止异常,强制执行 DNS 恢复', tag: 'SingBox');
|
||||||
|
try {
|
||||||
|
await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
|
||||||
|
} catch (dnsError) {
|
||||||
|
KRLogUtil.kr_e('❌ 强制 DNS 恢复失败: $dnsError', tag: 'SingBox');
|
||||||
|
} finally {
|
||||||
|
// 🔧 P0修复2: 异常路径也要重置标志位
|
||||||
|
_dnsBackedUp = false;
|
||||||
|
KRLogUtil.kr_i('🔄 异常后重置 DNS 备份标志位', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 不手动设置状态,信任 libcore 的状态管理
|
// 不手动设置状态,信任 libcore 的状态管理
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -1284,10 +1718,25 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> kr_restart() async {
|
Future<void> kr_restart() async {
|
||||||
KRLogUtil.kr_i("restart");
|
KRLogUtil.kr_i('🔄 重启 SingBox...', tag: 'SingBox');
|
||||||
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
|
|
||||||
KRLogUtil.kr_e('重启失败: $err');
|
// 🔧 修复:使用 stop + start 而不是 restart
|
||||||
}).run();
|
// 这样可以触发 Windows DNS 恢复和备份逻辑
|
||||||
|
try {
|
||||||
|
// 1. 先停止(会触发 DNS 恢复)
|
||||||
|
await kr_stop();
|
||||||
|
|
||||||
|
// 2. 等待完全停止
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// 3. 重新启动(会触发 DNS 备份)
|
||||||
|
await kr_start();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ SingBox 重启完成', tag: 'SingBox');
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 重启失败: $e', tag: 'SingBox');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//// 设置出站模式
|
//// 设置出站模式
|
||||||
@@ -1429,9 +1878,11 @@ class KRSingBoxImp {
|
|||||||
await _kr_ensureCommandClientInitialized();
|
await _kr_ensureCommandClientInitialized();
|
||||||
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 关键修复:使用 await 等待节点选择完成
|
// 🔧 关键修复:使用正确的 group tag
|
||||||
KRLogUtil.kr_i('⏳ 调用 selectOutbound("select", "$tag")...', tag: 'SingBox');
|
// libcore 生成的selector组的tag是"proxy"而不是"select"
|
||||||
await _kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50);
|
final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
|
||||||
|
KRLogUtil.kr_i('⏳ 调用 selectOutbound("$selectorGroupTag", "$tag")...', tag: 'SingBox');
|
||||||
|
await _kr_selectOutboundWithRetry(selectorGroupTag, tag, maxAttempts: 3, initialDelay: 50);
|
||||||
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 新增:验证节点切换是否生效
|
// 🔧 新增:验证节点切换是否生效
|
||||||
@@ -1465,87 +1916,6 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🌍 国家级节点选择
|
|
||||||
/// 如果传入 'auto',则调用原有 kr_selectOutbound('auto')
|
|
||||||
/// 否则根据国家名筛选该国节点,自动选择延迟最低的节点
|
|
||||||
Future<void> kr_selectCountry(String country) async {
|
|
||||||
KRLogUtil.kr_i('🌎 [v2.1] 开始选择国家: $country', tag: 'SingBox');
|
|
||||||
|
|
||||||
if (country == 'auto') {
|
|
||||||
KRLogUtil.kr_i('🌀 国家为 auto,执行自动节点选择逻辑', tag: 'SingBox');
|
|
||||||
await kr_selectOutbound('auto');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kr_activeGroups.isEmpty) {
|
|
||||||
KRLogUtil.kr_w('⚠️ kr_activeGroups 当前为空,无法进行国家选择', tag: 'SingBox');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*final selectGroup = kr_activeGroups.firstWhere(
|
|
||||||
(group) => group.tag == 'select',
|
|
||||||
orElse: () => throw Exception('未找到 "select" 组'),
|
|
||||||
);
|
|
||||||
|
|
||||||
final countryNodes = selectGroup.items.where((item) {
|
|
||||||
final nodeCountry = item.options?['country'] ?? '';
|
|
||||||
return nodeCountry.toString().toLowerCase() == country.toLowerCase();
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
if (countryNodes.isEmpty) {
|
|
||||||
KRLogUtil.kr_w('⚠️ 未找到该国家的节点: $country', tag: 'SingBox');
|
|
||||||
for (var item in selectGroup.items) {
|
|
||||||
KRLogUtil.kr_d(
|
|
||||||
' 可用节点: ${item.tag} (${item.options?['country'] ?? '未知'})',
|
|
||||||
tag: 'SingBox',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('🇨🇭 找到 ${countryNodes.length} 个属于 "$country" 的节点', tag: 'SingBox');
|
|
||||||
|
|
||||||
// 按延迟排序(无延迟则 9999)
|
|
||||||
countryNodes.sort((a, b) =>
|
|
||||||
(a.urlTestDelay ?? 9999).compareTo(b.urlTestDelay ?? 9999));
|
|
||||||
|
|
||||||
final fastestNode = countryNodes.first;
|
|
||||||
final selectedTag = fastestNode.tag;
|
|
||||||
final delay = fastestNode.urlTestDelay ?? -1;
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 选中该国家最快节点: $selectedTag (${delay}ms)', tag: 'SingBox');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await KRSecureStorage().kr_saveData(key: 'selected_country', value: country);
|
|
||||||
await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: selectedTag);
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('⚠️ 保存国家/节点信息失败: $e', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _kr_ensureCommandClientInitialized();
|
|
||||||
await _kr_selectOutboundWithRetry('select', selectedTag,
|
|
||||||
maxAttempts: 3, initialDelay: 100);
|
|
||||||
KRLogUtil.kr_i('✅ 国家内节点切换成功: $selectedTag', tag: 'SingBox');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('❌ 国家内节点选择失败: $e', tag: 'SingBox');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
|
|
||||||
_nodeSelectionTimer?.cancel();
|
|
||||||
KRLogUtil.kr_i('🔁 启动定时器防止节点被 auto 覆盖', tag: 'SingBox');
|
|
||||||
_nodeSelectionTimer =
|
|
||||||
Timer.periodic(const Duration(seconds: 20), (timer) async {
|
|
||||||
try {
|
|
||||||
await kr_singBox.selectOutbound('select', selectedTag).run();
|
|
||||||
KRLogUtil.kr_d('🔁 定时确认国家内节点: $selectedTag', tag: 'SingBox');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('🔁 定时确认失败: $e', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
});*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 配合文件地址
|
/// 配合文件地址
|
||||||
|
|
||||||
Directory get directory =>
|
Directory get directory =>
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ enum KRCountry {
|
|||||||
ru('俄罗斯'),
|
ru('俄罗斯'),
|
||||||
id('印度尼西亚'),
|
id('印度尼西亚'),
|
||||||
tr('土耳其'),
|
tr('土耳其'),
|
||||||
br('巴西');
|
br('巴西'),
|
||||||
|
other('全局代理'); // ✅ 新增:全局代理模式(所有流量走代理,不添加国家直连规则)
|
||||||
|
|
||||||
final String kr_name;
|
final String kr_name;
|
||||||
const KRCountry(this.kr_name);
|
const KRCountry(this.kr_name);
|
||||||
@@ -131,6 +132,8 @@ class KRCountryUtil {
|
|||||||
return AppTranslations.kr_country.tr;
|
return AppTranslations.kr_country.tr;
|
||||||
case KRCountry.br:
|
case KRCountry.br:
|
||||||
return AppTranslations.kr_country.br;
|
return AppTranslations.kr_country.br;
|
||||||
|
case KRCountry.other:
|
||||||
|
return '全局代理'; // ✅ 新增:全局代理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +1,151 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
import 'kr_log_util.dart';
|
||||||
|
|
||||||
/// 延迟测试工具类
|
/// 真正的节点延迟测试工具
|
||||||
/// 提供真实的 TCP 连接延迟测试功能
|
|
||||||
class KRLatencyTester {
|
class KRLatencyTester {
|
||||||
/// 测试单个节点的延迟
|
/// TCP 连接测试延迟(真实测试)
|
||||||
///
|
/// 返回延迟毫秒数,失败返回 65535
|
||||||
/// 参数:
|
static Future<int> testTcpLatency({
|
||||||
/// - host: 主机地址
|
|
||||||
/// - port: 端口号
|
|
||||||
/// - timeout: 超时时间(毫秒)
|
|
||||||
///
|
|
||||||
/// 返回:
|
|
||||||
/// - 延迟时间(毫秒),如果失败返回 65535
|
|
||||||
static Future<int> testNode({
|
|
||||||
required String host,
|
required String host,
|
||||||
required int port,
|
required int port,
|
||||||
int timeout = 5000,
|
Duration timeout = const Duration(seconds: 5),
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
Socket? socket;
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch();
|
||||||
|
|
||||||
final socket = await Socket.connect(
|
try {
|
||||||
|
KRLogUtil.kr_i('🔌 开始测试: $host:$port', tag: 'LatencyTester');
|
||||||
|
|
||||||
|
stopwatch.start();
|
||||||
|
|
||||||
|
// 尝试 TCP 连接
|
||||||
|
socket = await Socket.connect(
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
timeout: Duration(milliseconds: timeout),
|
|
||||||
).timeout(Duration(milliseconds: timeout));
|
|
||||||
|
|
||||||
stopwatch.stop();
|
|
||||||
|
|
||||||
// 立即关闭连接
|
|
||||||
await socket.close();
|
|
||||||
socket.destroy();
|
|
||||||
|
|
||||||
final latency = stopwatch.elapsedMilliseconds;
|
|
||||||
KRLogUtil.kr_i('✅ 延迟测试成功: $host:$port = ${latency}ms', tag: 'KRLatencyTester');
|
|
||||||
|
|
||||||
return latency;
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('❌ 延迟测试失败: $host:$port - $e', tag: 'KRLatencyTester');
|
|
||||||
return 65535; // 测试失败返回最大值
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 批量测试多个节点的延迟
|
|
||||||
///
|
|
||||||
/// 参数:
|
|
||||||
/// - nodes: 节点列表,格式为 [{"host": "example.com", "port": 443}]
|
|
||||||
/// - concurrency: 并发数量
|
|
||||||
/// - timeout: 超时时间(毫秒)
|
|
||||||
///
|
|
||||||
/// 返回:
|
|
||||||
/// - 测试结果映射,键为 "host:port",值为延迟时间
|
|
||||||
static Future<Map<String, int>> testMultipleNodes({
|
|
||||||
required List<MapEntry<String, SocketAddress>> nodes,
|
|
||||||
int concurrency = 10,
|
|
||||||
int timeout = 5000,
|
|
||||||
}) async {
|
|
||||||
final results = <String, int>{};
|
|
||||||
final semaphore = Completer<void>();
|
|
||||||
var activeCount = 0;
|
|
||||||
var completedCount = 0;
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('🚀 开始批量延迟测试,共 ${nodes.length} 个节点,并发数: $concurrency', tag: 'KRLatencyTester');
|
|
||||||
|
|
||||||
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,
|
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
results[key] = latency;
|
stopwatch.stop();
|
||||||
|
|
||||||
|
final latency = stopwatch.elapsedMilliseconds;
|
||||||
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
results[node.key] = 65535;
|
stopwatch.stop();
|
||||||
KRLogUtil.kr_e('❌ 节点测试异常: ${node.key} - $e', tag: 'KRLatencyTester');
|
KRLogUtil.kr_e('❌ 测试异常: $host:$port - $e', tag: 'LatencyTester');
|
||||||
|
return 65535;
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
// 确保关闭连接
|
||||||
|
try {
|
||||||
|
await socket?.close();
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略关闭错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量测试多个节点延迟(并发测试)
|
||||||
|
/// 返回 Map<节点tag, 延迟ms>
|
||||||
|
static Future<Map<String, int>> testMultipleNodes({
|
||||||
|
required List<MapEntry<String, SocketAddress>> nodes,
|
||||||
|
int concurrency = 10, // 并发数
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
}) async {
|
||||||
|
final results = <String, int>{};
|
||||||
|
final List<List<MapEntry<String, SocketAddress>>> batches = [];
|
||||||
|
|
||||||
|
// 分批处理
|
||||||
|
for (int i = 0; i < nodes.length; i += concurrency) {
|
||||||
|
batches.add(
|
||||||
|
nodes.sublist(i, i + concurrency > nodes.length ? nodes.length : i + concurrency)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
completedCount++;
|
completedCount++;
|
||||||
activeCount--;
|
|
||||||
|
|
||||||
if (completedCount >= nodes.length) {
|
if (completedCount % 5 == 0 || completedCount == nodes.length) {
|
||||||
semaphore.complete();
|
KRLogUtil.kr_i('📈 测试进度: $completedCount/${nodes.length}', tag: 'LatencyTester');
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分批处理节点
|
return MapEntry(tag, latency);
|
||||||
for (var i = 0; i < nodes.length; i += concurrency) {
|
}).toList();
|
||||||
final batch = nodes.skip(i).take(concurrency);
|
|
||||||
|
|
||||||
for (final node in batch) {
|
|
||||||
activeCount++;
|
|
||||||
processNode(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待当前批次完成
|
// 等待当前批次完成
|
||||||
if (i + concurrency < nodes.length) {
|
final batchResults = await Future.wait(futures);
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
|
||||||
|
// 收集结果
|
||||||
|
for (final result in batchResults) {
|
||||||
|
results[result.key] = result.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待所有任务完成
|
// 统计结果
|
||||||
await semaphore.future;
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 批量延迟测试完成,成功: ${results.length} 个', tag: 'KRLatencyTester');
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Socket 地址类
|
/// 节点地址信息
|
||||||
/// 表示网络地址和端口的组合
|
|
||||||
class SocketAddress {
|
class SocketAddress {
|
||||||
final String address;
|
final String host;
|
||||||
final int port;
|
final int port;
|
||||||
|
|
||||||
SocketAddress(this.address, this.port);
|
SocketAddress(this.host, this.port);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$address:$port';
|
String toString() => '$host:$port';
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,15 @@ class KRSecureStorage {
|
|||||||
try {
|
try {
|
||||||
final box = await _ensureBoxOpen();
|
final box = await _ensureBoxOpen();
|
||||||
await box.put(key, value);
|
await box.put(key, value);
|
||||||
|
|
||||||
|
// 🔧 关键修复:强制将数据立即写入磁盘,确保持久化
|
||||||
|
// 不调用 flush() 时,数据只在内存缓冲区中,应用关闭后会丢失!
|
||||||
|
await box.flush();
|
||||||
|
|
||||||
|
print('💾 [SecureStorage] 数据已保存并刷新到磁盘: $key');
|
||||||
KRLogUtil.kr_i('✅ 数据已保存: $key', tag: 'SecureStorage');
|
KRLogUtil.kr_i('✅ 数据已保存: $key', tag: 'SecureStorage');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('❌ [SecureStorage] 存储数据失败: $e');
|
||||||
KRLogUtil.kr_e('❌ 存储数据失败: $e', tag: 'SecureStorage');
|
KRLogUtil.kr_e('❌ 存储数据失败: $e', tag: 'SecureStorage');
|
||||||
rethrow; // 重新抛出异常,让调用者知道保存失败
|
rethrow; // 重新抛出异常,让调用者知道保存失败
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
|||||||
|
|
||||||
const WindowOptions windowOptions = WindowOptions(
|
const WindowOptions windowOptions = WindowOptions(
|
||||||
size: Size(800, 668),
|
size: Size(800, 668),
|
||||||
minimumSize: Size(800, 668),
|
minimumSize: Size(400, 334),
|
||||||
center: true,
|
center: true,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
skipTaskbar: false,
|
skipTaskbar: false,
|
||||||
@@ -47,15 +47,15 @@ class KRWindowManager with WindowListener, TrayListener {
|
|||||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||||
await windowManager.setTitle('HiFastVPN');
|
await windowManager.setTitle('HiFastVPN');
|
||||||
await windowManager.setSize(const Size(800, 668));
|
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.center();
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
// 阻止窗口关闭
|
// 阻止窗口关闭
|
||||||
await windowManager.setPreventClose(true);
|
await windowManager.setPreventClose(true);
|
||||||
} else {
|
} else {
|
||||||
await windowManager.setTitle('Kaer VPN');
|
await windowManager.setTitle('HiFastVPN');
|
||||||
await windowManager.setSize(const Size(800, 668));
|
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.center();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
|||||||
/// 初始化平台通道
|
/// 初始化平台通道
|
||||||
void _initPlatformChannel() {
|
void _initPlatformChannel() {
|
||||||
if (Platform.isMacOS) {
|
if (Platform.isMacOS) {
|
||||||
const platform = MethodChannel('kaer_vpn/terminate');
|
const platform = MethodChannel('hifast_vpn/terminate');
|
||||||
platform.setMethodCallHandler((call) async {
|
platform.setMethodCallHandler((call) async {
|
||||||
if (call.method == 'onTerminate') {
|
if (call.method == 'onTerminate') {
|
||||||
KRLogUtil.kr_i('收到应用终止通知');
|
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 bool showBackgroundImage;
|
||||||
|
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
/// resizeToAvoidBottomInset
|
/// resizeToAvoidBottomInset
|
||||||
final bool? resizeToAvoidBottomInset;
|
final bool? resizeToAvoidBottomInset;
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ class HIBaseScaffold extends StatelessWidget {
|
|||||||
this.showBackgroundImage = true,
|
this.showBackgroundImage = true,
|
||||||
this.topContentAreaHeight = 120.0,
|
this.topContentAreaHeight = 120.0,
|
||||||
this.resizeToAvoidBottomInset = false,
|
this.resizeToAvoidBottomInset = false,
|
||||||
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -71,7 +74,7 @@ class HIBaseScaffold extends StatelessWidget {
|
|||||||
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
|
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
|
||||||
backgroundColor: showBackgroundImage
|
backgroundColor: showBackgroundImage
|
||||||
? Colors.transparent
|
? Colors.transparent
|
||||||
: Theme.of(context).scaffoldBackgroundColor,
|
: (backgroundColor ?? Theme.of(context).scaffoldBackgroundColor),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
toolbarHeight: 0.0,
|
toolbarHeight: 0.0,
|
||||||
@@ -152,7 +155,7 @@ class HIBaseScaffold extends StatelessWidget {
|
|||||||
if (title != null || subtitle != null)
|
if (title != null || subtitle != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: topPadding +
|
top: topPadding +
|
||||||
(showBack ? 12.w : 20.w), // 顶部空出状态栏高度,并根据是否有返回按钮调整间距
|
(showBack ? 8 : 20), // 顶部空出状态栏高度,并根据是否有返回按钮调整间距
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -164,7 +167,7 @@ class HIBaseScaffold extends StatelessWidget {
|
|||||||
title!,
|
title!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black, // 在深色背景下使用白色
|
color: Colors.black, // 在深色背景下使用白色
|
||||||
fontSize: 24.sp,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -175,7 +178,7 @@ class HIBaseScaffold extends StatelessWidget {
|
|||||||
subtitle!,
|
subtitle!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black, // 副标题使用带透明度的白色
|
color: Colors.black, // 副标题使用带透明度的白色
|
||||||
fontSize: 14.sp,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w100,
|
fontWeight: FontWeight.w100,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // 已替换为自定义组件
|
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // 已替换为自定义组件
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
// import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
|
// import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
|
||||||
|
|
||||||
@@ -37,6 +38,12 @@ void main() async {
|
|||||||
// ✅ 等待 Flutter 确保窗口尺寸准备完毕(关键修复)
|
// ✅ 等待 Flutter 确保窗口尺寸准备完毕(关键修复)
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
await SystemChrome.setPreferredOrientations(
|
||||||
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化日志系统
|
// 初始化日志系统
|
||||||
KRLogUtil.kr_init();
|
KRLogUtil.kr_init();
|
||||||
|
|
||||||
@@ -175,6 +182,7 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
|
|||||||
Routes.HI_MENU,
|
Routes.HI_MENU,
|
||||||
Routes.KR_HOME,
|
Routes.KR_HOME,
|
||||||
Routes.HI_USER_INFO,
|
Routes.HI_USER_INFO,
|
||||||
|
Routes.KR_ORDER_STATUS,
|
||||||
];
|
];
|
||||||
print('routing.current${routing.current}');
|
print('routing.current${routing.current}');
|
||||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
||||||
|
|||||||
@@ -571,10 +571,10 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||||
@@ -583,7 +583,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
@@ -704,10 +704,10 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||||
@@ -716,7 +716,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -731,10 +731,10 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = NJRRF427XB;
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||||
@@ -743,7 +743,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
|
|
||||||
// 通知 Flutter 端应用即将终止
|
// 通知 Flutter 端应用即将终止
|
||||||
if let controller = NSApp.windows.first?.contentViewController as? FlutterViewController {
|
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)
|
channel.invokeMethod("onTerminate", arguments: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
PRODUCT_NAME = HiFastVPN
|
PRODUCT_NAME = HiFastVPN
|
||||||
|
|
||||||
// The application's bundle identifier
|
// The application's bundle identifier
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com
|
PRODUCT_BUNDLE_IDENTIFIER = app.taw.hifastvpn.mac
|
||||||
|
|
||||||
// The copyright displayed in application information
|
// The copyright displayed in application information
|
||||||
PRODUCT_COPYRIGHT = Copyright © 2025 HiFastVPN.com. All rights reserved.
|
PRODUCT_COPYRIGHT = Copyright © 2025 HiFastVPN.com. All rights reserved.
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
// 用于发布版本的签名设置
|
// 用于发布版本的签名设置
|
||||||
|
|
||||||
// 使用 Developer ID Application 证书进行签名(用于 App Store 外分发)
|
// 使用 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
|
// 开发团队 ID
|
||||||
DEVELOPMENT_TEAM = 3UR892FAP3
|
DEVELOPMENT_TEAM = NJRRF427XB
|
||||||
|
|
||||||
// 启用代码签名
|
// 启用代码签名
|
||||||
CODE_SIGN_STYLE = Manual
|
CODE_SIGN_STYLE = Manual
|
||||||
|
|||||||
@@ -18,22 +18,6 @@
|
|||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<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>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
title: Hiddify
|
title: HiFastVPN
|
||||||
|
volume: HiFastVPN
|
||||||
contents:
|
contents:
|
||||||
- x: 448
|
- x: 448
|
||||||
y: 344
|
y: 344
|
||||||
@@ -7,4 +8,4 @@ contents:
|
|||||||
- x: 192
|
- x: 192
|
||||||
y: 344
|
y: 344
|
||||||
type: file
|
type: file
|
||||||
path: Hiddify.app
|
path: HiFastVPN.app
|
||||||
|
|||||||
+1
-1
@@ -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
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.0.1+1
|
version: 0.0.4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.5.0 <4.0.0'
|
sdk: '>=3.5.0 <4.0.0'
|
||||||
|
|||||||
@@ -126,6 +126,11 @@ install(FILES "../libcore/bin/HiFastVPNCli.exe"
|
|||||||
COMPONENT Runtime
|
COMPONENT Runtime
|
||||||
OPTIONAL)
|
OPTIONAL)
|
||||||
|
|
||||||
|
install(FILES "runner/resources/app_icon.ico"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime
|
||||||
|
OPTIONAL)
|
||||||
|
|
||||||
|
|
||||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
install(FILES "${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%\msvcp140.dll" "%DLL_DIR%"
|
||||||
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
||||||
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
||||||
|
copy "%~dp0..\libcore\libcore.dll" "%DLL_DIR%"
|
||||||
|
|
||||||
echo DLL 文件复制完成!
|
echo DLL 文件复制完成!
|
||||||
pause
|
pause
|
||||||
@@ -14,6 +14,8 @@ Compression=lzma
|
|||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
SetupIconFile={{SETUP_ICON_FILE}}
|
SetupIconFile={{SETUP_ICON_FILE}}
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
|
LanguageDetectionMethod=uilanguage
|
||||||
|
UsePreviousTasks=no
|
||||||
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
||||||
ArchitecturesAllowed=x64
|
ArchitecturesAllowed=x64
|
||||||
ArchitecturesInstallIn64BitMode=x64
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
@@ -26,6 +28,8 @@ CloseApplications=force
|
|||||||
{% if locale == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %}
|
{% if locale == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %}
|
||||||
{% if locale == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.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' %}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 == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %}
|
||||||
{% if locale == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %}
|
{% if locale == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %}
|
||||||
{% if locale == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %}
|
{% if locale == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %}
|
||||||
@@ -50,7 +54,7 @@ CloseApplications=force
|
|||||||
|
|
||||||
[Tasks]
|
[Tasks]
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
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]
|
[Files]
|
||||||
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; 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
|
output_base_file_name: HiFastVPN.exe
|
||||||
create_desktop_icon: true
|
create_desktop_icon: true
|
||||||
install_dir_name: "{autopf64}\\HiFastVPN"
|
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:
|
locales:
|
||||||
|
- zh
|
||||||
|
- zh-cn
|
||||||
|
- zh_CN
|
||||||
- ar
|
- ar
|
||||||
- en
|
- en
|
||||||
- fa
|
- fa
|
||||||
@@ -15,3 +18,4 @@ locales:
|
|||||||
- pt
|
- pt
|
||||||
- tr
|
- tr
|
||||||
script_template: inno_setup.sas
|
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