diff --git a/assets/images/money-icon.svg b/assets/images/money-icon.svg index fd37f76..196c474 100644 --- a/assets/images/money-icon.svg +++ b/assets/images/money-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 690b64c..79e61d1 100755 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -26,6 +26,8 @@ PODS: - Flutter - ReachabilitySwift (5.2.4) - SAMKeychain (1.5.3) + - share_plus (0.0.1): + - Flutter - url_launcher_ios (0.0.1): - Flutter - webview_flutter_wkwebview (0.0.1): @@ -42,6 +44,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) @@ -69,6 +72,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" webview_flutter_wkwebview: @@ -87,6 +92,7 @@ SPEC CHECKSUMS: permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c + share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe webview_flutter_wkwebview: a4af96a051138e28e29f60101d094683b9f82188 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 194e40d..3f2a227 100755 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -758,11 +758,9 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB; + DEVELOPMENT_TEAM = NJRRF427XB; ENABLE_USER_SCRIPT_SANDBOXING = YES; EXCLUDED_ARCHS = armv7; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -788,7 +786,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -814,11 +811,9 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnelRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB; + DEVELOPMENT_TEAM = NJRRF427XB; ENABLE_USER_SCRIPT_SANDBOXING = YES; EXCLUDED_ARCHS = armv7; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -844,7 +839,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -868,11 +862,9 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB; + DEVELOPMENT_TEAM = NJRRF427XB; ENABLE_USER_SCRIPT_SANDBOXING = YES; EXCLUDED_ARCHS = armv7; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -898,7 +890,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -977,11 +968,9 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB; + DEVELOPMENT_TEAM = NJRRF427XB; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7; INFOPLIST_FILE = Runner/Info.plist; @@ -1013,7 +1002,6 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -1208,11 +1196,9 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB; + DEVELOPMENT_TEAM = NJRRF427XB; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; @@ -1244,7 +1230,6 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -1265,11 +1250,9 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB; + DEVELOPMENT_TEAM = NJRRF427XB; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7; INFOPLIST_FILE = Runner/Info.plist; @@ -1301,7 +1284,6 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/app/modules/kr_delete_account/views/kr_delete_account_view.dart b/lib/app/modules/kr_delete_account/views/kr_delete_account_view.dart index 79a9c08..23719d5 100755 --- a/lib/app/modules/kr_delete_account/views/kr_delete_account_view.dart +++ b/lib/app/modules/kr_delete_account/views/kr_delete_account_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:flutter/services.dart'; import 'package:kaer_with_panels/app/common/app_run_data.dart'; import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart'; import 'package:kaer_with_panels/app/widgets/kr_local_image.dart'; @@ -15,52 +16,57 @@ class KRDeleteAccountView extends GetView { @override Widget build(BuildContext context) { return HIBaseScaffold( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(40.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildUserInfoSection(), - SizedBox(height: 12.w), - // 验证码输入框 - ConstrainedBox( - constraints: BoxConstraints( - minHeight: 250.h, // 2. 设置最小高度为 300 (使用 .h 适配屏幕) - ), - // 3. 使用 Column 和 MainAxisAlignment.center 使其垂直居中 - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildVerificationCodeField(), // 4. 在这里调用,不修改其内部 - ], - ), - ), - SizedBox(height: 20.w), - GestureDetector( - onTap: () { - controller.requestDeleteAccount(); - }, - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 12.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.w), - border: - Border.all(color: const Color(0xFFFF2ED1), width: 2), + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(40.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildUserInfoSection(), + SizedBox(height: 12.w), + // 验证码输入框 + ConstrainedBox( + constraints: BoxConstraints( + minHeight: 250.h, // 2. 设置最小高度为 300 (使用 .h 适配屏幕) ), - alignment: Alignment.center, - child: Text( - '注销账户', - style: TextStyle( - color: const Color(0xFFFF2ED1), - fontSize: 16.sp, - fontWeight: FontWeight.w700, + // 3. 使用 Column 和 MainAxisAlignment.center 使其垂直居中 + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildVerificationCodeField(), // 4. 在这里调用,不修改其内部 + ], + ), + ), + SizedBox(height: 20.w), + GestureDetector( + onTap: () { + controller.requestDeleteAccount(); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 12.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.w), + border: + Border.all(color: const Color(0xFFFF2ED1), width: 2), + ), + alignment: Alignment.center, + child: Text( + '注销账户', + style: TextStyle( + color: const Color(0xFFFF2ED1), + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), ), ), ), - ), - /*SizedBox( + /*SizedBox( width: double.infinity, child: ElevatedButton( onPressed: controller.requestDeleteAccount, @@ -80,8 +86,9 @@ class KRDeleteAccountView extends GetView { ), ), ),*/ - SizedBox(height: 20.w), - ], + SizedBox(height: 20.w), + ], + ), ), ), ), @@ -129,11 +136,9 @@ class KRDeleteAccountView extends GetView { ); }), Obx(() { - final account = KRAppRunData.getInstance() - .kr_account - .value; - final isDeviceLogin = account != null && - account.startsWith('9000'); + final account = KRAppRunData.getInstance().kr_account.value; + final isDeviceLogin = + account != null && account.startsWith('9000'); final accountText = isDeviceLogin ? '待绑定' : '${KRAppRunData.getInstance().kr_account.value.toString()}'; @@ -164,11 +169,44 @@ class KRDeleteAccountView extends GetView { /// 构建注册页面的验证码输入框(包含间距) Widget _buildVerificationCodeField() { return SizedBox( - height: 50.w, // 固定高度 + height: 50.w, child: TextField( controller: controller.kr_codeController, keyboardType: TextInputType.number, textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.oneTimeCode], + enableSuggestions: false, + autocorrect: false, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (value) { + var v = value.replaceAll(RegExp(r"\s+"), ""); + if (v.length % 2 == 0 && v.isNotEmpty) { + final half = v.length ~/ 2; + final first = v.substring(0, half); + final second = v.substring(half); + if (first == second) { + v = first; + } + } + const maxLen = 6; + if (v.length > maxLen) { + v = v.substring(0, maxLen); + } + if (controller.kr_codeController.text != v) { + controller.kr_codeController.value = + controller.kr_codeController.value.copyWith( + text: v, + selection: TextSelection.collapsed(offset: v.length), + composing: TextRange.empty, + ); + } + if (v.isNotEmpty && (v.length >= 6)) { + FocusScope.of(Get.context!).unfocus(); + } + }, + onSubmitted: (_) { + FocusScope.of(Get.context!).unfocus(); + }, style: KrAppTextStyle( fontSize: 16, color: Colors.white, @@ -184,19 +222,20 @@ class KRDeleteAccountView extends GetView { EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w), border: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), - borderSide: BorderSide(color: Colors.white, width: 2), + borderSide: const BorderSide(color: Colors.white, width: 2), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), - borderSide: BorderSide(color: Colors.white, width: 2), + borderSide: const BorderSide(color: Colors.white, width: 2), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25.w), - borderSide: BorderSide(color: Colors.white, width: 2), + borderSide: const BorderSide(color: Colors.white, width: 2), ), isDense: true, suffixIconConstraints: BoxConstraints( - maxHeight: 50.w, // 限制最大高度 + maxHeight: 50.w, + maxWidth: 190.w, ), suffixIcon: Padding( padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), @@ -212,7 +251,7 @@ class KRDeleteAccountView extends GetView { color: controller.kr_canSendCode.value ? Theme.of(Get.context!).primaryColor : const Color(0xFFD5D5D5), - borderRadius: BorderRadius.circular(100.r), // 药丸状 + borderRadius: BorderRadius.circular(100.r), ), child: Text( controller.kr_canSendCode.value diff --git a/lib/app/modules/kr_home/controllers/kr_home_controller.dart b/lib/app/modules/kr_home/controllers/kr_home_controller.dart index 8288fa9..5f6a2f3 100755 --- a/lib/app/modules/kr_home/controllers/kr_home_controller.dart +++ b/lib/app/modules/kr_home/controllers/kr_home_controller.dart @@ -91,9 +91,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { final kr_isLatency = false.obs; /// 默认 - var kr_cutTag = 'auto'.obs; - var kr_cutSeletedTag = 'auto'.obs; - var kr_coutryText = 'auto'.obs; + var kr_cutTag = ''.obs; + var kr_cutSeletedTag = ''.obs; + var kr_coutryText = ''.obs; var kr_selectedCountryTag = 'auto'.obs; void kr_setSelectedCountryTag(String country) { @@ -216,6 +216,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { } await _kr_prepareCountrySelectionBeforeStart(); + final selectedAfter = + await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); + KRLogUtil.kr_i('准备后 SELECTED_NODE_TAG_autoConnect: ${selectedAfter ?? ''}', + tag: 'HomeController'); + KRLogUtil.kr_i('准备后 kr_currentNodeName_autoConnect: ${kr_currentNodeName.value}', + tag: 'HomeController'); + KRLogUtil.kr_i('准备后 kr_cutTag_autoConnect: ${kr_cutTag.value}', + tag: 'HomeController'); + KRLogUtil.kr_i('准备后 kr_cutSeletedTag_autoConnect: ${kr_cutSeletedTag.value}', + tag: 'HomeController'); + await kr_performNodeSwitch(selectedAfter!); await KRSingBoxImp.instance.kr_start(); KRLogUtil.kr_i('闪连自动连接执行完成', tag: 'QuickConnect'); } catch (e) { @@ -659,9 +670,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { break; case KRSubscribeServiceStatus.kr_success: KRLogUtil.kr_i('订阅服务成功', tag: 'HomeController'); - kr_cutTag.value = 'auto'; - kr_cutSeletedTag.value = 'auto'; - kr_currentNodeName.value = "auto"; + kr_cutTag.value = ''; + kr_cutSeletedTag.value = ''; + kr_currentNodeName.value = ""; if (kr_currentListStatus.value != KRHomeViewsListStatus.kr_none) { kr_currentListStatus.value = KRHomeViewsListStatus.kr_none; } else { @@ -779,8 +790,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { break; case SingboxStopping(): KRLogUtil.kr_i('🟠 状态: 正在停止', tag: 'HomeController'); - // 取消连接超时处理 - _cancelConnectionTimeout(); kr_connectText.value = AppTranslations.kr_home.disconnecting; kr_isConnected.value = false; kr_currentSpeed.value = "--"; @@ -885,6 +894,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController'); if (kDebugMode) {} await _kr_prepareCountrySelectionBeforeStart(); + final selectedAfter = + await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); + KRLogUtil.kr_i('准备后 SELECTED_NODE_TAG: ${selectedAfter ?? ''}', + tag: 'HomeController'); + KRLogUtil.kr_i('准备后 kr_currentNodeName: ${kr_currentNodeName.value}', + tag: 'HomeController'); + KRLogUtil.kr_i('准备后 kr_cutTag: ${kr_cutTag.value}', + tag: 'HomeController'); + KRLogUtil.kr_i('准备后 kr_cutSeletedTag: ${kr_cutSeletedTag.value}', + tag: 'HomeController'); + await kr_performNodeSwitch(selectedAfter!); await KRSingBoxImp.instance.kr_start(); KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController'); if (kDebugMode) {} @@ -958,6 +978,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verify', tag: 'CountrySelect'); + kr_currentNodeName.value = best; + kr_cutTag.value = best; kr_cutSeletedTag.value = best; kr_updateConnectionInfo(); return; @@ -974,6 +996,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verifyA', tag: 'CountrySelect'); + kr_currentNodeName.value = bestAfterTest; + kr_cutTag.value = bestAfterTest; kr_cutSeletedTag.value = bestAfterTest; kr_updateConnectionInfo(); return; @@ -991,6 +1015,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verifyB', tag: 'CountrySelect'); + kr_currentNodeName.value = fallback; + kr_cutTag.value = fallback; kr_cutSeletedTag.value = fallback; kr_updateConnectionInfo(); } else { @@ -1027,6 +1053,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verify2', tag: 'CountrySelect'); + kr_currentNodeName.value = bestInCountry; + kr_cutTag.value = bestInCountry; kr_cutSeletedTag.value = bestInCountry; kr_updateConnectionInfo(); } else { @@ -1084,22 +1112,33 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { void _kr_handleSelectorProxy(dynamic element, List allGroups) { try { KRLogUtil.kr_d( - '处理选择器代理 - 当前选择: ${element.selected}, 用户选择: ${kr_cutTag.value}', + '处理选择器代理 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}, 切换中: $_isSwitchingNode', tag: 'HomeController'); - // 🔧 关键修复:仅更新 UI 状态,不要触发重新选择,避免死循环 - // 更新 kr_cutSeletedTag 以反映实际选中的节点 - if (element.selected.isNotEmpty) { - kr_cutSeletedTag.value = element.selected; + // 🔧 关键修复:如果正在切换节点中,不要用内核返回值覆盖 UI + // 这是导致"跳两次"问题的根本原因 + if (_isSwitchingNode) { + KRLogUtil.kr_d('⏳ 节点切换进行中,跳过内核状态同步', tag: 'HomeController'); + // 只更新延迟值,不更新选中状态 + _kr_updateNodeLatency(element); + return; } // 如果用户手动选择了节点(不是auto) if (kr_cutTag.value != "auto") { + // 🔧 修复:用户手动选择时,不要用内核返回的 selected 覆盖用户选择 + // 只有当内核返回的节点与用户选择一致时,才更新 kr_cutSeletedTag + if (element.selected == kr_cutTag.value) { + kr_cutSeletedTag.value = element.selected; + } _kr_handleManualMode(element); return; } - // 默认auto模式处理 + // 默认auto模式处理 - 此时可以用内核返回值更新 UI + if (element.selected.isNotEmpty) { + kr_cutSeletedTag.value = element.selected; + } _kr_handleAutoMode(element, allGroups); } catch (e) { KRLogUtil.kr_e('处理选择器代理出错: $e', tag: 'HomeController'); @@ -1109,15 +1148,25 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { /// 处理手动模式 void _kr_handleManualMode(dynamic element) { try { - KRLogUtil.kr_d('处理手动模式 - 选择: ${element.selected}', tag: 'HomeController'); + KRLogUtil.kr_d('处理手动模式 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}', tag: 'HomeController'); - // 🔧 关键修复:仅更新 UI 状态,不要重新选择节点,避免死循环 - kr_cutSeletedTag.value = element.selected; - // 更新延迟值 + // 🔧 关键修复:不要用内核返回的 selected 覆盖用户手动选择 + // 只有当内核返回的节点与用户选择一致时,才更新相关状态 + // 这可以防止 UI 跳动 + + // 更新延迟值(这个始终需要更新) _kr_updateNodeLatency(element); - kr_currentNodeName.value = - kr_truncateText(element.selected, maxLength: 25); - // kr_moveToSelectedNode(); + + // 只有当内核确认切换成功时,才更新 UI 状态 + if (element.selected == kr_cutTag.value) { + kr_cutSeletedTag.value = element.selected; + kr_currentNodeName.value = kr_truncateText(element.selected, maxLength: 25); + // kr_moveToSelectedNode(); + KRLogUtil.kr_d('✅ 内核确认节点切换成功: ${element.selected}', tag: 'HomeController'); + } else { + // 内核返回的节点与用户选择不一致,保持用户选择的显示 + KRLogUtil.kr_d('⏳ 等待内核切换到用户选择的节点: ${kr_cutTag.value}', tag: 'HomeController'); + } } catch (e) { KRLogUtil.kr_e('处理手动模式出错: $e', tag: 'HomeController'); } @@ -1315,12 +1364,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 🔒 节流控制:2秒内的重复切换请求直接忽略 final now = DateTime.now(); - if (_lastSwitchTime != null && - now.difference(_lastSwitchTime!) < _switchThrottleDuration) { - final remainingTime = _switchThrottleDuration.inMilliseconds - - now.difference(_lastSwitchTime!).inMilliseconds; - KRLogUtil.kr_w('⚠️ 切换过于频繁,请等待 ${remainingTime}ms', - tag: 'HomeController'); + if (_lastSwitchTime != null && now.difference(_lastSwitchTime!) < _switchThrottleDuration) { + final remainingTime = _switchThrottleDuration.inMilliseconds - now.difference(_lastSwitchTime!).inMilliseconds; + KRLogUtil.kr_w('⚠️ 切换过于频繁,请等待 ${remainingTime}ms', tag: 'HomeController'); KRCommonUtil.kr_showToast('切换过于频繁,请稍后再试'); return false; } @@ -1347,9 +1393,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 🔧 修复:保存节点选择以便VPN启动时应用 KRLogUtil.kr_i('💾 保存节点选择以便稍后应用: $tag', tag: 'HomeController'); - KRSecureStorage() - .kr_saveData(key: 'SELECTED_NODE_TAG', value: tag) - .then((_) { + KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag).then((_) { KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'HomeController'); }).catchError((e) { KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'HomeController'); @@ -1358,91 +1402,93 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { return true; } - // 4. VPN已连接,需要重启VPN以断开现有连接并应用新节点 + // 4. VPN已连接,使用热切换方式(selectOutbound)切换节点 try { - KRLogUtil.kr_i('🔌 VPN已连接,将重启VPN以断开现有连接: $tag', tag: 'HomeController'); + KRLogUtil.kr_i('🔌 VPN已连接,使用热切换方式切换节点: $tag', tag: 'HomeController'); - // 🔧 诊断:打印当前活动组信息 - KRLogUtil.kr_i( - '📊 当前活动组数量: ${KRSingBoxImp.instance.kr_activeGroups.length}', - tag: 'HomeController'); - for (var group in KRSingBoxImp.instance.kr_activeGroups) { - KRLogUtil.kr_i( - '📋 活动组: tag=${group.tag}, type=${group.type}, 节点数=${group.items.length}', - tag: 'HomeController'); - for (var item in group.items) { - if (item.tag == tag) { - KRLogUtil.kr_i('✅ 找到目标节点: ${item.tag}', tag: 'HomeController'); - } - } - } - - // 🔧 修复:VPN已连接时,设置延迟为-1(切换中状态),显示"正在连接" + // 🔧 设置切换中状态,显示"正在连接" kr_currentNodeLatency.value = -1; kr_isLatency.value = true; // 显示加载动画 - // 🔧 关键修复:保存新节点选择 + // 🔧 保存新节点选择 KRLogUtil.kr_i('💾 保存新节点选择: $tag', tag: 'HomeController'); - await KRSecureStorage() - .kr_saveData(key: 'SELECTED_NODE_TAG', value: tag); + await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag); - // 🔧 方案A优化:重启VPN连接以断开所有现有长连接 - KRLogUtil.kr_i('🔄 [优化] 停止VPN连接以断开现有连接...', tag: 'HomeController'); - await KRSingBoxImp.instance.kr_stop(); // 先停止VPN(已跳过DNS恢复) + // 🚀 核心改进:使用 selectOutbound 进行热切换(参考 hiddify-app) + // 优势:不重启VPN,保持连接状态,切换瞬间完成,VPN开关不闪烁 + KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...', tag: 'HomeController'); - // 🚀 优化:减少等待时间(DNS操作已优化,无需过长等待) - KRLogUtil.kr_i('⏳ [优化] 等待VPN完全停止(800ms)...', tag: 'HomeController'); - await Future.delayed( - const Duration(milliseconds: 800)); // 从1500ms减少到800ms + // 🔧 关键修复:确定正确的 selector 组 tag + // selectOutbound(groupTag, outboundTag) - 第一个参数是组的tag,不是节点的tag + final activeGroups = KRSingBoxImp.instance.kr_activeGroups; + String selectorGroupTag = 'select'; // 默认值 - KRLogUtil.kr_i('🔄 [优化] 启动VPN并应用新节点: $tag', tag: 'HomeController'); - await KRSingBoxImp.instance.kr_start(); // 重新启动VPN(已跳过DNS备份) + // 查找 selector 类型的组 + for (var group in activeGroups) { + if (group.type == ProxyType.selector) { + selectorGroupTag = group.tag; + KRLogUtil.kr_i('🔍 找到 selector 组: $selectorGroupTag', tag: 'HomeController'); + break; + } + } - // 🚀 优化:减少等待时间(DNS操作已优化) - KRLogUtil.kr_i('⏳ [优化] 等待VPN完全启动(1200ms)...', tag: 'HomeController'); - await Future.delayed( - const Duration(milliseconds: 1200)); // 从2500ms减少到1200ms + KRLogUtil.kr_i('📡 调用 selectOutbound("$selectorGroupTag", "$tag")', tag: 'HomeController'); - // 后台切换成功,更新UI + // 调用 sing-box 的 selectOutbound API + final result = await KRSingBoxImp.instance.kr_singBox + .selectOutbound(selectorGroupTag, tag) + .run(); + + // 处理切换结果 + result.fold( + (error) { + // 切换失败 + KRLogUtil.kr_e('❌ selectOutbound 调用失败: $error', tag: 'HomeController'); + throw Exception('节点切换失败: $error'); + }, + (_) { + // 切换成功 + KRLogUtil.kr_i('✅ selectOutbound 调用成功', tag: 'HomeController'); + }, + ); + + // 后台切换成功,立即更新UI(乐观更新) kr_cutSeletedTag.value = tag; kr_updateConnectionInfo(); - kr_moveToSelectedNode(); + // kr_moveToSelectedNode(); - // 🚀 优化:减少验证等待时间 - KRLogUtil.kr_i('⏳ [优化] 等待活动组更新(300ms)...', tag: 'HomeController'); - await Future.delayed( - const Duration(milliseconds: 300)); // 从500ms减少到300ms + // 🔧 短暂等待以确保内核状态同步(相比重启,等待时间大幅缩短) + KRLogUtil.kr_i('⏳ [热切换] 等待内核状态同步(200ms)...', tag: 'HomeController'); + await Future.delayed(const Duration(milliseconds: 200)); - // 🚀 方案A增强:验证节点是否真正切换成功 - KRLogUtil.kr_i('🔍 [增强] 验证节点切换是否成功...', tag: 'HomeController'); + // 🔍 验证节点是否真正切换成功 + KRLogUtil.kr_i('🔍 [验证] 检查节点切换结果...', tag: 'HomeController'); try { - // 刷新活动组信息 - final activeGroups = KRSingBoxImp.instance.kr_activeGroups; - final selectGroup = activeGroups.firstWhere( - (group) => group.tag == 'select', - orElse: () => throw Exception('未找到 select 组'), + final updatedGroups = KRSingBoxImp.instance.kr_activeGroups; + final selectGroup = updatedGroups.firstWhere( + (group) => group.type == ProxyType.selector, + orElse: () => throw Exception('未找到 selector 组'), ); - KRLogUtil.kr_i('📊 [增强] Select组当前选中: ${selectGroup.selected}', - tag: 'HomeController'); - KRLogUtil.kr_i('📊 [增强] 目标节点: $tag', tag: 'HomeController'); + KRLogUtil.kr_i('📊 [验证] ${selectGroup.tag}组当前选中: ${selectGroup.selected}', tag: 'HomeController'); + KRLogUtil.kr_i('📊 [验证] 目标节点: $tag', tag: 'HomeController'); if (selectGroup.selected != tag) { - KRLogUtil.kr_w('⚠️ [增强] 节点选择验证失败,实际选中: ${selectGroup.selected}', - tag: 'HomeController'); + KRLogUtil.kr_w('⚠️ [验证] 节点选择验证失败,实际选中: ${selectGroup.selected}', tag: 'HomeController'); // 不抛出异常,但记录警告 } else { - KRLogUtil.kr_i('✅ [增强] 节点选择验证成功!', tag: 'HomeController'); + KRLogUtil.kr_i('✅ [验证] 节点选择验证成功!', tag: 'HomeController'); } } catch (e) { - KRLogUtil.kr_w('⚠️ [增强] 节点验证过程出错: $e', tag: 'HomeController'); + KRLogUtil.kr_w('⚠️ [验证] 节点验证过程出错: $e', tag: 'HomeController'); } // 更新延迟信息 _kr_updateLatencyOnConnected(); - KRLogUtil.kr_i('✅ 节点切换成功(已重启VPN断开旧连接): $tag', tag: 'HomeController'); + KRLogUtil.kr_i('✅ 节点热切换成功,VPN保持连接: $tag', tag: 'HomeController'); return true; + } catch (switchError) { // 后台切换失败,恢复到原节点 KRLogUtil.kr_e('❌ 后台节点切换失败: $switchError', tag: 'HomeController'); @@ -1454,8 +1500,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 恢复原节点选择 try { - await KRSecureStorage() - .kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag); + await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag); } catch (e) { KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'HomeController'); } diff --git a/lib/app/modules/kr_home/views/hi_subscription_corner_button.dart b/lib/app/modules/kr_home/views/hi_subscription_corner_button.dart index 34cfad7..18c5d17 100644 --- a/lib/app/modules/kr_home/views/hi_subscription_corner_button.dart +++ b/lib/app/modules/kr_home/views/hi_subscription_corner_button.dart @@ -55,7 +55,7 @@ class CustomCircleRoute extends PageRouteBuilder { required this.child, required this.startOffset, required this.transitionColor, - this.customDuration = const Duration(milliseconds: 1500), + this.customDuration = const Duration(milliseconds: 250), }) : super( opaque: false, transitionDuration: customDuration, @@ -162,7 +162,7 @@ class _HISubscriptionCornerButtonState extends State child: KRPurchaseMembershipView(), startOffset: startOffset, transitionColor: transitionColor, - customDuration: const Duration(milliseconds: 1500), + customDuration: const Duration(milliseconds: 500), ), ).then((_) { // 页面返回后恢复默认主题色 diff --git a/lib/app/modules/kr_invite/views/kr_invite_view.dart b/lib/app/modules/kr_invite/views/kr_invite_view.dart index d9774e1..3ccaf37 100755 --- a/lib/app/modules/kr_invite/views/kr_invite_view.dart +++ b/lib/app/modules/kr_invite/views/kr_invite_view.dart @@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart'; import 'package:kaer_with_panels/app/widgets/kr_local_image.dart'; import '../controllers/kr_invite_controller.dart'; import 'package:flutter/services.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:kaer_with_panels/app/utils/kr_common_util.dart'; import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart'; import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart'; @@ -15,7 +16,6 @@ class KRInviteView extends GetView { @override Widget build(BuildContext context) { - final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; return HIBaseScaffold( @@ -35,7 +35,8 @@ class KRInviteView extends GetView { // 🟢 第一行:奖励说明 Container( width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w), + padding: + EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w), decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(25.r), @@ -68,13 +69,14 @@ class KRInviteView extends GetView { SizedBox(height: 26.w), // 🟢 第二行:我的邀请码 Container( - padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w), + padding: + EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w), decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 2.0), borderRadius: BorderRadius.circular(1000.r), ), child: Obx( - () => Row( + () => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( @@ -113,12 +115,27 @@ class KRInviteView extends GetView { ), onPressed: () { if (controller.kr_referCode.value.isNotEmpty) { - Clipboard.setData( - ClipboardData(text: controller.kr_referCode.value), - ); - KRCommonUtil.kr_showToast( - AppTranslations.kr_invite.inviteCodeCopied, - ); + if (GetPlatform.isIOS) { + final code = controller.kr_referCode.value; + final text = '#您的好友邀请您使用Hi快网络加速器\n' + '安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n' + '$code\n' + '您和您的好友将会分别获得3天免费时长\n\n' + '点击此处进入下载页面\n' + '或在浏览器输入hifastvpn.com下载#'; + Share.share( + text, + subject: '直接分享Hi快VPN邀请链接', + ); + } else { + Clipboard.setData( + ClipboardData( + text: controller.kr_referCode.value), + ); + KRCommonUtil.kr_showToast( + AppTranslations.kr_invite.inviteCodeCopied, + ); + } } }, ), @@ -143,24 +160,29 @@ class KRInviteView extends GetView { TextField( controller: controller.otherInviteCodeController, textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold), decoration: InputDecoration( hintText: '填入邀请人邀请码兑换免费时长...', hintStyle: const TextStyle(color: Color(0xFFA6A6A6)), filled: true, fillColor: Colors.transparent, - contentPadding: EdgeInsets.symmetric(horizontal: 22.w), + contentPadding: + EdgeInsets.symmetric(horizontal: 22.w), border: OutlineInputBorder( borderRadius: BorderRadius.circular(1000.r), - borderSide: const BorderSide(color: Colors.white, width: 2.0), + borderSide: const BorderSide( + color: Colors.white, width: 2.0), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(1000.r), - borderSide: const BorderSide(color: Colors.white, width: 2.0), + borderSide: const BorderSide( + color: Colors.white, width: 2.0), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(1000.r), - borderSide: const BorderSide(color: Colors.white, width: 2.0), + borderSide: const BorderSide( + color: Colors.white, width: 2.0), ), constraints: BoxConstraints(maxHeight: 50.h), ), @@ -199,8 +221,7 @@ class KRInviteView extends GetView { ), // 5. 将 HIHelpEntrance 作为 Stack 的直接子元素,它将恢复其原有的绝对定位能力 - if (!isKeyboardVisible) - const HIHelpEntrance(), + if (!isKeyboardVisible) const HIHelpEntrance(), ], ), ); diff --git a/lib/app/modules/kr_login/views/kr_login_view.dart b/lib/app/modules/kr_login/views/kr_login_view.dart index d9884f0..ccb0128 100755 --- a/lib/app/modules/kr_login/views/kr_login_view.dart +++ b/lib/app/modules/kr_login/views/kr_login_view.dart @@ -13,6 +13,7 @@ import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_control import 'package:kaer_with_panels/app/services/kr_site_config_service.dart'; import 'package:flutter/foundation.dart'; import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart'; +import 'package:flutter/services.dart'; class KRLoginView extends GetView { const KRLoginView({super.key}); @@ -27,23 +28,29 @@ class KRLoginView extends GetView { resizeToAvoidBottomInset: true, child: Stack( children: [ - SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 40.w, - ), - child: Column( - children: [ - // Text( - // '${controller.kr_loginStatus.value}', - // style: TextStyle(color: Colors.white), // 使用 TextStyle() - // ), - SizedBox(height: 20.w), - _buildUserInfoSection(), - SizedBox(height: 12.w), - _buildContentByEntry(), - SizedBox(height: 100.w), // 为底部帮助按钮留出空间 - ], + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 40.w, + ), + child: Column( + children: [ + // Text( + // '${controller.kr_loginStatus.value}', + // style: TextStyle(color: Colors.white), // 使用 TextStyle() + // ), + SizedBox(height: 20.w), + _buildUserInfoSection(), + SizedBox(height: 12.w), + _buildContentByEntry(), + SizedBox(height: 100.w), // 为底部帮助按钮留出空间 + ], + ), ), ), ), @@ -79,11 +86,9 @@ class KRLoginView extends GetView { mainAxisAlignment: MainAxisAlignment.center, children: [ Obx(() { - final account = KRAppRunData.getInstance() - .kr_account - .value; - final isDeviceLogin = account != null && - account.startsWith('9000'); + final account = KRAppRunData.getInstance().kr_account.value; + final isDeviceLogin = + account != null && account.startsWith('9000'); final accountText = isDeviceLogin ? '待绑定' : '${KRAppRunData.getInstance().kr_account.value.toString()}'; @@ -270,6 +275,41 @@ class KRLoginView extends GetView { height: 50.w, // 固定高度 child: TextField( controller: controller.codeController, + keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.oneTimeCode], + enableSuggestions: false, + autocorrect: false, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (value) { + var v = value.replaceAll(RegExp("\\s+"), ""); + if (v.length % 2 == 0 && v.isNotEmpty) { + final half = v.length ~/ 2; + final first = v.substring(0, half); + final second = v.substring(half); + if (first == second) { + v = first; + } + } + const maxLen = 6; + if (v.length > maxLen) { + v = v.substring(0, maxLen); + } + if (controller.codeController.text != v) { + controller.codeController.value = + controller.codeController.value.copyWith( + text: v, + selection: TextSelection.collapsed(offset: v.length), + composing: TextRange.empty, + ); + } + if (v.isNotEmpty && (v.length >= 6)) { + FocusScope.of(Get.context!).unfocus(); + } + }, + onSubmitted: (_) { + FocusScope.of(Get.context!).unfocus(); + }, style: KrAppTextStyle( fontSize: 16, color: Colors.white, diff --git a/lib/app/modules/kr_message/views/kr_message_view.dart b/lib/app/modules/kr_message/views/kr_message_view.dart index 15f2f28..400d06b 100755 --- a/lib/app/modules/kr_message/views/kr_message_view.dart +++ b/lib/app/modules/kr_message/views/kr_message_view.dart @@ -13,6 +13,7 @@ import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart'; import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart'; import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart'; import '../../../widgets/kr_simple_loading.dart'; + class KRMessageView extends GetView { const KRMessageView({super.key}); @@ -25,62 +26,77 @@ class KRMessageView extends GetView { children: [ // 主要内容区域 Obx(() { - return EasyRefresh( - controller: controller.refreshController, - onRefresh: controller.kr_onRefresh, - onLoad: controller.kr_onLoadMore, - header: ClassicHeader( - dragText: '下拉刷新', - armedText: '释放刷新', - readyText: '正在刷新...', - processingText: '正在刷新...', - processedText: '刷新成功', - failedText: '刷新失败', - messageText: '最后更新于 %T', - textStyle: TextStyle(color: Colors.white.withOpacity(0.7)), - messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp), - iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)), - ), - // 3. 添加 Footer 以显示上拉加载的UI提示 - footer: ClassicFooter( - dragText: '上拉加载', - armedText: '释放加载', - readyText: '正在加载...', - processingText: '正在加载...', - processedText: '加载成功', - failedText: '加载失败', - noMoreText: '没有更多数据了', - messageText: '最后更新于 %T', - textStyle: TextStyle(color: Colors.white.withOpacity(0.7)), - messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp), - iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)), - ), - child: Padding( - padding: EdgeInsets.only(right: 0.w, bottom: 90.w), // 为HIHelpEntrance留出空间 - child: HiFixedScrollbar( - controller: scrollController, - isShowScrollbar: controller.kr_messages.length > 0, - child: ListView.builder( - controller: scrollController, - padding: EdgeInsets.symmetric(horizontal: 40.w), - itemCount: controller.kr_messages.length, - itemBuilder: (context, index) { - final message = controller.kr_messages[index]; - final collapsibleItemData = HICollapsibleItem( - title: message.title, - content: [message.content], - ); - return Padding( - padding: EdgeInsets.only(bottom: 10.w), - child: HICollapsibleItemWidget(item: collapsibleItemData), - ); - }, + return Column( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.only(bottom: 90.w), + child: EasyRefresh( + controller: controller.refreshController, + onRefresh: controller.kr_onRefresh, + onLoad: controller.kr_onLoadMore, + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '正在刷新...', + processingText: '正在刷新...', + processedText: '刷新成功', + failedText: '刷新失败', + messageText: '最后更新于 %T', + textStyle: + TextStyle(color: Colors.white.withOpacity(0.7)), + messageStyle: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 12.sp), + iconTheme: + IconThemeData(color: Colors.white.withOpacity(0.7)), + ), + footer: ClassicFooter( + dragText: '上拉加载', + armedText: '释放加载', + readyText: '正在加载...', + processingText: '正在加载...', + processedText: '加载成功', + failedText: '加载失败', + noMoreText: '没有更多数据了', + messageText: '最后更新于 %T', + textStyle: + TextStyle(color: Colors.white.withOpacity(0.7)), + messageStyle: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 12.sp), + iconTheme: + IconThemeData(color: Colors.white.withOpacity(0.7)), + ), + child: Padding( + padding: EdgeInsets.only(right: 0.w), + child: HiFixedScrollbar( + controller: scrollController, + child: ListView.builder( + controller: scrollController, + padding: EdgeInsets.symmetric(horizontal: 40.w), + itemCount: controller.kr_messages.length, + itemBuilder: (context, index) { + final message = controller.kr_messages[index]; + final collapsibleItemData = HICollapsibleItem( + title: message.title, + content: [message.content], + ); + return Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: HICollapsibleItemWidget( + item: collapsibleItemData), + ); + }, + ), + ), + ), + ), ), ), - ), + ], ); - - },), + }), // 底部帮助入口 const HIHelpEntrance(), ], diff --git a/lib/app/modules/kr_order_status/views/kr_order_status_view.dart b/lib/app/modules/kr_order_status/views/kr_order_status_view.dart index 43c276c..d3a1072 100755 --- a/lib/app/modules/kr_order_status/views/kr_order_status_view.dart +++ b/lib/app/modules/kr_order_status/views/kr_order_status_view.dart @@ -25,13 +25,10 @@ class KROrderStatusView extends GetView { children: [ Center( child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.symmetric(horizontal: 0.w), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // 状态图标 - // _buildStatusIcon(), - SizedBox(height: 122.h), // 状态文本 _buildStatusText(), SizedBox(height: 16.h), diff --git a/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart b/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart index 4557d16..10ba3ee 100755 --- a/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart +++ b/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart @@ -92,14 +92,14 @@ class KRPurchaseMembershipView extends GetView { padding: EdgeInsets.only(bottom: 8.0), child: SizedBox( - height: 140.0, + height: 130.0, child: _kr_buildPlanOptionCard( plan, controller .kr_selectedPlanIndex.value, discountIndex, context, - index == 0, + index, ), ), ); @@ -167,11 +167,12 @@ class KRPurchaseMembershipView extends GetView { int planIndex, int? discountIndex, BuildContext context, - bool isFirst, + int index, ) { // 由于移除了 Obx,isSelected 的值在构建时就固定了。 // 它将不再响应用户的点击事件来改变UI。 // 根据您的要求,isSelected 的值与 isFirst 相反。 + bool isFirst = 0 == index; bool isSelected = !isFirst; return GestureDetector( @@ -190,16 +191,16 @@ class KRPurchaseMembershipView extends GetView { child: ClipRRect( borderRadius: BorderRadius.circular(38.0), // 内部圆角 child: Container( - padding: EdgeInsets.fromLTRB(45.0, 10.0, 0.0, 10.0), + padding: EdgeInsets.fromLTRB(45.0, 0.0, 0.0, 0.0), decoration: BoxDecoration( color: isSelected ? Colors.black : Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(38.0), ), child: Stack( + alignment: Alignment.centerLeft, clipBehavior: Clip.none, children: [ Column( - mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -214,25 +215,80 @@ class KRPurchaseMembershipView extends GetView { : Theme.of(context).textTheme.bodyMedium?.color, ), ), - Text( - '¥${controller.kr_getPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}', - style: TextStyle( - fontSize: 40, - height: 1, - fontWeight: FontWeight.w600, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + KrLocalImage( + imageName: 'money-icon', + imageType: ImageType.svg, + width: 36, + height: 36, color: isSelected ? Colors.white : Theme.of(context).textTheme.bodyMedium?.color, - )), - Text( - '约¥${controller.kr_getDayPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}/天', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + ), + SizedBox(width: 2), + Text( + controller + .kr_getPlanPrice(plan, + discountIndex: discountIndex) + .toStringAsFixed(2), + style: TextStyle( + fontSize: 40, + height: 1, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : Theme.of(context) + .textTheme + .bodyMedium + ?.color, + ), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '约', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : Theme.of(context) + .textTheme + .bodyMedium + ?.color, + ), + ), + SizedBox(width: 2), + KrLocalImage( + imageName: 'money-icon', + imageType: ImageType.svg, + width: 16, + height: 16, color: isSelected ? Colors.white : Theme.of(context).textTheme.bodyMedium?.color, - )), + ), + SizedBox(width: 2), + Text( + '${controller.kr_getDayPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}/天', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : Theme.of(context) + .textTheme + .bodyMedium + ?.color, + ), + ), + ], + ), ]), if (!isFirst) Positioned( @@ -242,7 +298,7 @@ class KRPurchaseMembershipView extends GetView { angle: 0.785398, // 45度角 (π/4) child: Container( // 调整位置,让标签的一部分显示在容器外 - transform: Matrix4.translationValues(30, -10, 0), + transform: Matrix4.translationValues(40, -10, 0), width: 130, // 适当的宽度 height: 20, // 固定高度 decoration: BoxDecoration( @@ -254,7 +310,9 @@ class KRPurchaseMembershipView extends GetView { style: TextStyle( color: Colors.black, // 白色文字更清晰 fontSize: 14, - fontWeight: FontWeight.w300, + fontWeight: index == 1 + ? FontWeight.w300 + : FontWeight.w600, ), textAlign: TextAlign.center, ), diff --git a/lib/app/routes/transitions/slide_transparent_transition.dart b/lib/app/routes/transitions/slide_transparent_transition.dart index 33dce25..8a95edb 100644 --- a/lib/app/routes/transitions/slide_transparent_transition.dart +++ b/lib/app/routes/transitions/slide_transparent_transition.dart @@ -110,7 +110,7 @@ class SlideTransparentTransition extends CustomTransition { SlideTransparentTransition({ this.direction = SlideDirection.rightToLeft, - this.duration = const Duration(milliseconds: 2000), + this.duration = const Duration(milliseconds: 1000), }); @override diff --git a/lib/app/routes/transitions/transition_config.dart b/lib/app/routes/transitions/transition_config.dart index 6d3d23b..96d9eca 100644 --- a/lib/app/routes/transitions/transition_config.dart +++ b/lib/app/routes/transitions/transition_config.dart @@ -4,7 +4,7 @@ import 'slide_transparent_transition.dart'; /// 全局页面过渡动画配置类 class TransitionConfig { - static Duration _defaultDuration = const Duration(milliseconds: 350); + static Duration _defaultDuration = const Duration(milliseconds: 150); static const SlideDirection _defaultDirection = SlideDirection.rightToLeft; static const Curve _defaultCurve = Curves.easeOutCubic; diff --git a/lib/app/services/global_overlay_service.dart b/lib/app/services/global_overlay_service.dart index da68cfa..7694ef0 100644 --- a/lib/app/services/global_overlay_service.dart +++ b/lib/app/services/global_overlay_service.dart @@ -88,15 +88,9 @@ class GlobalOverlayService extends GetxService { }, child: IgnorePointer( ignoring: false, // 不阻断事件 - child: SizedBox( - width: 44, - height: 44, - child: KrLocalImage( - imageName: 'money-icon', - width: 44, - height: 44, - imageType: ImageType.svg, - ), + child: KrLocalImage( + imageName: 'money-icon', + imageType: ImageType.svg, ), ), ), diff --git a/lib/app/services/singbox_imp/kr_sing_box_imp.dart b/lib/app/services/singbox_imp/kr_sing_box_imp.dart index 6c07d54..4845d29 100755 --- a/lib/app/services/singbox_imp/kr_sing_box_imp.dart +++ b/lib/app/services/singbox_imp/kr_sing_box_imp.dart @@ -185,14 +185,14 @@ class KRSingBoxImp { KRLogUtil.kr_i('iOS 路径获取完成: $paths'); kr_configDics = ( - baseDir: Directory(paths?["base"]! as String), - workingDir: Directory(paths?["working"]! as String), - tempDir: Directory(paths?["temp"]! as String), + baseDir: Directory(paths?["base"]! as String), + workingDir: Directory(paths?["working"]! as String), + tempDir: Directory(paths?["temp"]! as String), ); } else { final baseDir = await getApplicationSupportDirectory(); final workingDir = - Platform.isAndroid ? await getExternalStorageDirectory() : baseDir; + Platform.isAndroid ? await getExternalStorageDirectory() : baseDir; final tempDir = await getTemporaryDirectory(); // Windows 路径规范化:确保使用正确的路径分隔符 @@ -200,7 +200,8 @@ class KRSingBoxImp { if (Platform.isWindows) { final normalized = dir.path.replaceAll('/', '\\'); if (normalized != dir.path) { - KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized', tag: 'SingBox'); + KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized', + tag: 'SingBox'); return Directory(normalized); } } @@ -208,16 +209,17 @@ class KRSingBoxImp { } kr_configDics = ( - baseDir: normalizePath(baseDir), - workingDir: normalizePath(workingDir!), - tempDir: normalizePath(tempDir), + baseDir: normalizePath(baseDir), + workingDir: normalizePath(workingDir!), + tempDir: normalizePath(tempDir), ); KRLogUtil.kr_i('其他平台路径初始化完成'); } KRLogUtil.kr_i('开始创建目录'); KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox'); - KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox'); + KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', + tag: 'SingBox'); KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox'); // 确保所有目录都存在 @@ -253,12 +255,14 @@ class KRSingBoxImp { break; } else { retryCount++; - KRLogUtil.kr_i('⚠️ data 目录创建后验证失败,重试 $retryCount/$maxRetries', tag: 'SingBox'); + KRLogUtil.kr_i('⚠️ data 目录创建后验证失败,重试 $retryCount/$maxRetries', + tag: 'SingBox'); await Future.delayed(const Duration(milliseconds: 200)); } } catch (e) { retryCount++; - KRLogUtil.kr_e('❌ 创建 data 目录失败 (尝试 $retryCount/$maxRetries): $e', tag: 'SingBox'); + KRLogUtil.kr_e('❌ 创建 data 目录失败 (尝试 $retryCount/$maxRetries): $e', + tag: 'SingBox'); if (retryCount >= maxRetries) { throw Exception('无法创建 libcore 数据库目录: ${dataDir.path},错误: $e'); } @@ -294,32 +298,40 @@ class KRSingBoxImp { // libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data" // 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败 if (!kr_configDics.workingDir.existsSync()) { - final error = '❌ workingDir 不存在,无法调用 setup(): ${kr_configDics.workingDir.path}'; + final error = + '❌ workingDir 不存在,无法调用 setup(): ${kr_configDics.workingDir.path}'; KRLogUtil.kr_e(error, tag: 'SingBox'); throw Exception(error); } // 验证 workingDir 可读可写 try { - final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir')); + final testWorkingFile = + File(p.join(kr_configDics.workingDir.path, '.test_working_dir')); await testWorkingFile.writeAsString('test'); await testWorkingFile.delete(); KRLogUtil.kr_i('✅ workingDir 写入权限验证通过', tag: 'SingBox'); } catch (e) { - final error = '❌ workingDir 无写入权限: ${kr_configDics.workingDir.path}, 错误: $e'; + final error = + '❌ workingDir 无写入权限: ${kr_configDics.workingDir.path}, 错误: $e'; KRLogUtil.kr_e(error, tag: 'SingBox'); throw Exception(error); } - final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data')); + final finalDataDir = + Directory(p.join(kr_configDics.workingDir.path, 'data')); if (!finalDataDir.existsSync()) { KRLogUtil.kr_e('❌ 最终验证失败:data 目录不存在', tag: 'SingBox'); KRLogUtil.kr_e('路径: ${finalDataDir.path}', tag: 'SingBox'); - KRLogUtil.kr_e('workingDir 是否存在: ${kr_configDics.workingDir.existsSync()}', tag: 'SingBox'); + KRLogUtil.kr_e( + 'workingDir 是否存在: ${kr_configDics.workingDir.existsSync()}', + tag: 'SingBox'); if (kr_configDics.workingDir.existsSync()) { try { final workingDirContents = kr_configDics.workingDir.listSync(); - KRLogUtil.kr_e('workingDir 内容: ${workingDirContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox'); + KRLogUtil.kr_e( + 'workingDir 内容: ${workingDirContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', + tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('无法列出 workingDir 内容: $e', tag: 'SingBox'); } @@ -338,11 +350,13 @@ class KRSingBoxImp { // 不抛出异常,让 setup() 自己处理 } - final configsDir = Directory(p.join(kr_configDics.workingDir.path, "configs")); + final configsDir = + Directory(p.join(kr_configDics.workingDir.path, "configs")); if (!configsDir.existsSync()) { try { await configsDir.create(recursive: true); - KRLogUtil.kr_i('✅ 已创建 configs 目录: ${configsDir.path}', tag: 'SingBox'); + KRLogUtil.kr_i('✅ 已创建 configs 目录: ${configsDir.path}', + tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('⚠️ configs 目录创建失败: $e', tag: 'SingBox'); // 不抛出异常,继续初始化 @@ -354,8 +368,10 @@ class KRSingBoxImp { // 特别处理 extensionData.db 文件 (Windows特定) if (Platform.isWindows) { try { - final extensionDataDbPath = p.join(finalDataDir.path, 'extensionData.db'); - KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath', tag: 'SingBox'); + final extensionDataDbPath = + p.join(finalDataDir.path, 'extensionData.db'); + KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath', + tag: 'SingBox'); // 确保 extensionData.db 的父目录存在 final extensionDataParent = Directory(p.dirname(extensionDataDbPath)); @@ -365,7 +381,8 @@ class KRSingBoxImp { } // 测试文件创建权限 - final testFile = File(p.join(extensionDataParent.path, '.test_extension')); + final testFile = + File(p.join(extensionDataParent.path, '.test_extension')); await testFile.writeAsString('test'); await testFile.delete(); KRLogUtil.kr_i('✅ extensionData 目录权限验证通过', tag: 'SingBox'); @@ -377,18 +394,26 @@ class KRSingBoxImp { KRLogUtil.kr_i('✅ 目录创建完成', tag: 'SingBox'); KRLogUtil.kr_i('开始设置 SingBox', tag: 'SingBox'); - KRLogUtil.kr_i(' - baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox'); - KRLogUtil.kr_i(' - workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox'); - KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox'); - KRLogUtil.kr_i(' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}', tag: 'SingBox'); - KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox'); + KRLogUtil.kr_i(' - baseDir: ${kr_configDics.baseDir.path}', + tag: 'SingBox'); + KRLogUtil.kr_i(' - workingDir: ${kr_configDics.workingDir.path}', + tag: 'SingBox'); + KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}', + tag: 'SingBox'); + KRLogUtil.kr_i( + ' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}', + tag: 'SingBox'); + KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', + tag: 'SingBox'); // 在 Windows 上,列出 data 目录内容(如果有文件) if (Platform.isWindows && finalDataDir.existsSync()) { try { final dataContents = finalDataDir.listSync(); if (dataContents.isNotEmpty) { - KRLogUtil.kr_i(' - data 目录现有文件: ${dataContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox'); + KRLogUtil.kr_i( + ' - data 目录现有文件: ${dataContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', + tag: 'SingBox'); } else { KRLogUtil.kr_i(' - data 目录为空', tag: 'SingBox'); } @@ -397,19 +422,23 @@ class KRSingBoxImp { } } - KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox'); + KRLogUtil.kr_i( + ' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', + tag: 'SingBox'); KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox'); // Windows 特定:验证路径格式是否正确 if (Platform.isWindows) { final workingPath = kr_configDics.workingDir.path; if (workingPath.contains('/')) { - KRLogUtil.kr_e('⚠️ 警告:Windows 路径包含正斜杠,可能导致问题: $workingPath', tag: 'SingBox'); + KRLogUtil.kr_e('⚠️ 警告:Windows 路径包含正斜杠,可能导致问题: $workingPath', + tag: 'SingBox'); } // 确保路径使用反斜杠(Windows 标准) final normalizedPath = workingPath.replaceAll('/', '\\'); if (normalizedPath != workingPath) { - KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath', tag: 'SingBox'); + KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath', + tag: 'SingBox'); } } @@ -418,11 +447,11 @@ class KRSingBoxImp { KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox'); final setupResult = await kr_singBox.setup(kr_configDics, false).run(); setupResult.match( - (error) { + (error) { KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox'); throw Exception('FFI setup 失败: $error'); }, - (_) { + (_) { KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox'); }, ); @@ -457,19 +486,25 @@ class KRSingBoxImp { KRLogUtil.kr_e('🔍 Windows 路径诊断信息:', tag: 'SingBox'); KRLogUtil.kr_e(' - workingDir: ${workingDir.path}', tag: 'SingBox'); - KRLogUtil.kr_e(' - workingDir 存在: ${workingDir.existsSync()}', tag: 'SingBox'); + KRLogUtil.kr_e(' - workingDir 存在: ${workingDir.existsSync()}', + tag: 'SingBox'); KRLogUtil.kr_e(' - data 目录: ${dataDir.path}', tag: 'SingBox'); - KRLogUtil.kr_e(' - data 目录存在: ${dataDir.existsSync()}', tag: 'SingBox'); + KRLogUtil.kr_e(' - data 目录存在: ${dataDir.existsSync()}', + tag: 'SingBox'); KRLogUtil.kr_e(' - configs 目录: ${configsDir.path}', tag: 'SingBox'); - KRLogUtil.kr_e(' - configs 目录存在: ${configsDir.existsSync()}', tag: 'SingBox'); + KRLogUtil.kr_e(' - configs 目录存在: ${configsDir.existsSync()}', + tag: 'SingBox'); // 检查父目录内容 if (workingDir.existsSync()) { try { final contents = workingDir.listSync(); - KRLogUtil.kr_e(' - workingDir 内容: ${contents.map((e) => "${e.path.split(Platform.pathSeparator).last}${e is Directory ? "/" : ""}").join(", ")}', tag: 'SingBox'); + KRLogUtil.kr_e( + ' - workingDir 内容: ${contents.map((e) => "${e.path.split(Platform.pathSeparator).last}${e is Directory ? "/" : ""}").join(", ")}', + tag: 'SingBox'); } catch (listErr) { - KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr', tag: 'SingBox'); + KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr', + tag: 'SingBox'); } } } catch (diagErr) { @@ -492,7 +527,8 @@ class KRSingBoxImp { Future _kr_extractGeositeFiles() async { try { // 创建 geosite 目录 - final geositeDir = Directory(p.join(kr_configDics.workingDir.path, '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'); @@ -509,7 +545,8 @@ class KRSingBoxImp { // 检查文件是否已存在 if (targetFile.existsSync()) { final fileSize = await targetFile.length(); - KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox'); + KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', + tag: 'SingBox'); continue; } @@ -522,7 +559,8 @@ class KRSingBoxImp { await targetFile.writeAsBytes(bytes); final writtenSize = await targetFile.length(); - KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox'); + KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', + tag: 'SingBox'); } KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox'); @@ -542,24 +580,27 @@ class KRSingBoxImp { // 🔧 关键修复:全局代理模式下,region 强制设为 'other',libcore 就不会生成国家直连规则 final String effectiveRegion; if (kr_connectionType.value == KRConnectionType.global) { - effectiveRegion = 'other'; // 全局代理:不添加任何国家规则 + effectiveRegion = 'other'; // 全局代理:不添加任何国家规则 KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other,所有流量走代理', tag: 'SingBox'); } else { - effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家 + effectiveRegion = + KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家 KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox'); } final op = { - "region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region - "block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截 + "region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region + "block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截 "use-xray-core-when-possible": false, - "execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写 - "log-level": "info", // 调试阶段使用 info,生产环境改为 warn + "execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写 + "log-level": "info", // 调试阶段使用 info,生产环境改为 warn "resolve-destination": false, - "ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only) - "remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁 + "ipv6-mode": + "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only) + "remote-dns-address": + "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁 "remote-dns-domain-strategy": "prefer_ipv4", - "direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析 + "direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析 "direct-dns-domain-strategy": "prefer_ipv4", "mixed-port": kr_port, "tproxy-port": kr_port, @@ -567,14 +608,15 @@ class KRSingBoxImp { "tun-implementation": "gvisor", "mtu": 9000, "strict-route": true, - "connection-test-url": "http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点 + "connection-test-url": + "http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点 "url-test-interval": 30, "enable-clash-api": true, "clash-api-port": 36756, "enable-tun": Platform.isIOS || Platform.isAndroid, "enable-tun-service": false, "set-system-proxy": - Platform.isWindows || Platform.isLinux || Platform.isMacOS, + Platform.isWindows || Platform.isLinux || Platform.isMacOS, "bypass-lan": false, "allow-connection-from-lan": false, "enable-fake-dns": false, @@ -628,11 +670,14 @@ class KRSingBoxImp { // 🔧 调试日志:确认自定义规则已添加 final rules = op["rules"] as List?; - KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', tag: 'SingBox'); + KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', + tag: 'SingBox'); if (rules != null && rules.isNotEmpty) { for (var rule in rules) { final ruleMap = rule as Map; - KRLogUtil.kr_i(' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', tag: 'SingBox'); + KRLogUtil.kr_i( + ' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', + tag: 'SingBox'); } } @@ -641,21 +686,16 @@ class KRSingBoxImp { List> _kr_buildHiddifyRules() { final rules = >[]; - rules.add({ - "domains": "domain:api.hifast.biz", - "outbound": "bypass" - }); + 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" - }); + rules.add({"domains": "domain:$d", "outbound": "bypass"}); } KRLogUtil.kr_i('✅ 节点域名白名单数量: ${nodeDomains.length}', tag: 'SingBox'); - KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}', tag: 'SingBox'); + KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}', + tag: 'SingBox'); return rules; } @@ -664,21 +704,25 @@ class KRSingBoxImp { void addFromOutbound(Map o) { final server = o['server']?.toString(); - if (server != null && server.isNotEmpty && InternetAddress.tryParse(server) == null) { + 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) { + if (sni != null && + sni.isNotEmpty && + InternetAddress.tryParse(sni) == null) { set.add(sni.toLowerCase()); } } } for (final g in kr_outbounds) { - addFromOutbound(g); + addFromOutbound(g); } return set; @@ -706,7 +750,7 @@ class KRSingBoxImp { _kr_subscriptions.add( kr_singBox.watchStatus().listen( - (status) { + (status) { if (kDebugMode) { print('🔵 收到 Native 状态更新: ${status.runtimeType}'); } @@ -744,7 +788,7 @@ class KRSingBoxImp { // 所以外层必须有 try-catch final stream = kr_singBox.watchStats(); final subscription = stream.listen( - (stats) { + (stats) { kr_stats.value = stats; }, onError: (error) { @@ -769,7 +813,7 @@ class KRSingBoxImp { _kr_subscriptions.add( kr_singBox.watchActiveGroups().listen( - (groups) { + (groups) { print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}'); KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox'); kr_activeGroups.value = groups; @@ -777,10 +821,14 @@ class KRSingBoxImp { // 详细打印每个组的信息 for (int i = 0; i < groups.length; i++) { final group = groups[i]; - KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox'); + KRLogUtil.kr_i( + '📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', + tag: 'SingBox'); for (int j = 0; j < group.items.length; j++) { final item = group.items[j]; - KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox'); + KRLogUtil.kr_i( + ' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', + tag: 'SingBox'); } } @@ -796,13 +844,14 @@ class KRSingBoxImp { _kr_subscriptions.add( kr_singBox.watchGroups().listen( - (groups) { + (groups) { print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}'); kr_allGroups.value = groups; // 打印每个组的基本信息 for (int i = 0; i < groups.length; i++) { final group = groups[i]; - print('[watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}'); + print( + '[watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}'); } }, onError: (error) { @@ -812,7 +861,8 @@ class KRSingBoxImp { cancelOnError: false, ), ); - print('[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}'); + print( + '[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}'); } /// 验证节点选择是否生效 @@ -824,7 +874,7 @@ class KRSingBoxImp { // 查找 "select" 组 final selectGroup = kr_activeGroups.firstWhere( - (group) => group.tag == 'select', + (group) => group.tag == 'select', orElse: () => throw Exception('未找到 "select" 选择器组'), ); @@ -853,7 +903,8 @@ class KRSingBoxImp { throw Exception('节点切换失败:实际选中 ${selectGroup.selected},期望 $targetTag'); } - KRLogUtil.kr_i('✅ 节点切换验证成功: ${selectGroup.selected} == $targetTag', tag: 'SingBox'); + KRLogUtil.kr_i('✅ 节点切换验证成功: ${selectGroup.selected} == $targetTag', + tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ 节点验证异常: $e', tag: 'SingBox'); // 不抛出异常,只记录日志,避免阻塞流程 @@ -864,11 +915,11 @@ class KRSingBoxImp { /// /// 确保 command.sock 准备好后再执行节点选择 Future _kr_selectOutboundWithRetry( - String groupTag, - String outboundTag, { - int maxAttempts = 3, - int initialDelay = 100, - }) async { + String groupTag, + String outboundTag, { + int maxAttempts = 3, + int initialDelay = 100, + }) async { int attempt = 0; int delay = initialDelay; @@ -931,7 +982,8 @@ class KRSingBoxImp { try { // 先验证 command.sock 是否可访问 - final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock')); + final socketFile = + File(p.join(kr_configDics.baseDir.path, 'command.sock')); KRLogUtil.kr_i('🔍 检查 socket 文件: ${socketFile.path}', tag: 'SingBox'); if (!socketFile.existsSync()) { @@ -949,7 +1001,7 @@ class KRSingBoxImp { // 这样可以避免阻塞当前的异步执行 try { KRLogUtil.kr_i('📊 订阅统计数据流...', tag: 'SingBox'); - await Future.delayed(Duration.zero); // 让出 UI 线程 + await Future.delayed(Duration.zero); // 让出 UI 线程 _kr_subscribeToStats(); statsSubscribed = true; KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox'); @@ -959,7 +1011,7 @@ class KRSingBoxImp { try { KRLogUtil.kr_i('📋 订阅分组数据流...', tag: 'SingBox'); - await Future.delayed(Duration.zero); // 让出 UI 线程 + await Future.delayed(Duration.zero); // 让出 UI 线程 _kr_subscribeToGroups(); groupsSubscribed = true; KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox'); @@ -981,7 +1033,8 @@ class KRSingBoxImp { throw Exception('订阅列表为空,command client 未成功连接'); } - KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}', tag: 'SingBox'); + KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}', + tag: 'SingBox'); return; } catch (e, stackTrace) { // 详细记录失败原因和堆栈信息 @@ -1024,7 +1077,8 @@ class KRSingBoxImp { await Future.delayed(const Duration(milliseconds: 1500)); // 检查 socket 文件 - final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock')); + final socketFile = + File(p.join(kr_configDics.baseDir.path, 'command.sock')); if (!socketFile.existsSync()) { KRLogUtil.kr_w('⚠️ command.sock 尚未创建,预连接取消', tag: 'SingBox'); return; @@ -1047,7 +1101,6 @@ class KRSingBoxImp { } catch (e) { KRLogUtil.kr_w('⚠️ 分组流预订阅失败(正常,UI 调用时会重试): $e', tag: 'SingBox'); } - } catch (e) { // 静默失败,不影响主流程 KRLogUtil.kr_w('⚠️ 后台预连接任务失败(不影响正常使用): $e', tag: 'SingBox'); @@ -1061,7 +1114,8 @@ class KRSingBoxImp { Future _kr_ensureCommandClientInitialized() async { // 如果已经有订阅,说明 command client 已初始化 if (_kr_subscriptions.isNotEmpty) { - KRLogUtil.kr_i('✅ Command client 已初始化(订阅数: ${_kr_subscriptions.length})', tag: 'SingBox'); + KRLogUtil.kr_i('✅ Command client 已初始化(订阅数: ${_kr_subscriptions.length})', + tag: 'SingBox'); return; } @@ -1096,7 +1150,8 @@ class KRSingBoxImp { // 等待 command client 初始化 await Future.delayed(const Duration(milliseconds: 2000)); - final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); + final savedNode = + await KRSecureStorage().kr_readData(key: _keySelectedNode); if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto') { KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox'); @@ -1104,7 +1159,8 @@ class KRSingBoxImp { await _kr_selectOutboundWithRetry("select", savedNode); KRLogUtil.kr_i('✅ 节点恢复完成', tag: 'SingBox'); } catch (e) { - KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e', tag: 'SingBox'); + KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e', + tag: 'SingBox'); } } else { KRLogUtil.kr_i('ℹ️ 使用默认节点选择 (auto)', tag: 'SingBox'); @@ -1151,7 +1207,8 @@ class KRSingBoxImp { KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox'); KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox'); KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox'); - KRLogUtil.kr_i(' - server_port: ${outbound['server_port']}', tag: 'SingBox'); + KRLogUtil.kr_i(' - server_port: ${outbound['server_port']}', + tag: 'SingBox'); if (outbound['method'] != null) { KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox'); } @@ -1159,10 +1216,14 @@ class KRSingBoxImp { KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox'); } 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'); } if (outbound['uuid'] != null) { - KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox'); + KRLogUtil.kr_i( + ' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', + tag: 'SingBox'); } KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox'); } @@ -1171,22 +1232,21 @@ class KRSingBoxImp { kr_outbounds = outbounds.where((outbound) { final type = outbound['type']; if (type == 'hysteria2' || type == 'hysteria') { - KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox'); + KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', + tag: 'SingBox'); return false; } return true; }).toList(); - KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox'); + KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', + tag: 'SingBox'); // 🔧 修复:生成完整的 SingBox 配置 // 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理 // 现在保存完整配置,包含所有必需字段 final Map fullConfig = { - "log": { - "level": "debug", - "timestamp": true - }, + "log": {"level": "debug", "timestamp": true}, "dns": { "servers": [ { @@ -1194,13 +1254,9 @@ class KRSingBoxImp { "address": "https://1.1.1.1/dns-query", "address_resolver": "dns-direct" }, - { - "tag": "dns-direct", - "address": "local", - "detour": "direct" - } + {"tag": "dns-direct", "address": "local", "detour": "direct"} ], - "rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则 + "rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则 "final": "dns-remote", "strategy": "prefer_ipv4" }, @@ -1222,26 +1278,18 @@ class KRSingBoxImp { "type": "selector", "tag": "proxy", "outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(), - "default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct", + "default": + kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct", }, ...kr_outbounds, - { - "type": "direct", - "tag": "direct" - }, - { - "type": "block", - "tag": "block" - }, - { - "type": "dns", - "tag": "dns-out" - } + {"type": "direct", "tag": "direct"}, + {"type": "block", "tag": "block"}, + {"type": "dns", "tag": "dns-out"} ], "route": { - "rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则 - "rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集 - "final": "proxy", // 🔧 修复:使用 selector 组作为默认出站 + "rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则 + "rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集 + "final": "proxy", // 🔧 修复:使用 selector 组作为默认出站 "auto_detect_interface": true } }; @@ -1252,7 +1300,9 @@ class KRSingBoxImp { KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', 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'); + KRLogUtil.kr_i( + '📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', + tag: 'SingBox'); await file.writeAsString(mapStr); await temp.writeAsString(mapStr); @@ -1276,7 +1326,8 @@ class KRSingBoxImp { // 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则 // 如果是"全局代理"模式,即使选择了国家也不添加直连规则 - if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') { + if (kr_connectionType.value == KRConnectionType.rule && + currentCountryCode != 'other') { rules.add({ "rule_set": [ "geoip-$currentCountryCode", @@ -1285,7 +1336,8 @@ class KRSingBoxImp { "server": "dns-direct" }); - KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox'); + KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', + tag: 'SingBox'); } else if (kr_connectionType.value == KRConnectionType.global) { KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则,所有DNS查询走代理', tag: 'SingBox'); } @@ -1299,10 +1351,7 @@ class KRSingBoxImp { final rules = >[]; // 基础规则: DNS 查询走 dns-out - rules.add({ - "protocol": "dns", - "outbound": "dns-out" - }); + rules.add({"protocol": "dns", "outbound": "dns-out"}); // ✅ 自定义域名直连规则(优先级高,放在前面) // rules.add({ @@ -1314,7 +1363,8 @@ class KRSingBoxImp { // 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则 // 如果是"全局代理"模式,即使选择了国家也不添加直连规则 - if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') { + if (kr_connectionType.value == KRConnectionType.rule && + currentCountryCode != 'other') { rules.add({ "rule_set": [ "geoip-$currentCountryCode", @@ -1323,7 +1373,8 @@ class KRSingBoxImp { "outbound": "direct" }); - KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox'); + KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', + tag: 'SingBox'); } else if (kr_connectionType.value == KRConnectionType.global) { KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox'); } @@ -1338,10 +1389,13 @@ class KRSingBoxImp { // 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集 // 如果是"全局代理"模式,即使选择了国家也不加载规则集 - if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') { + 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')); + 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()) { // ✅ 使用本地文件 @@ -1349,7 +1403,7 @@ class KRSingBoxImp { "type": "local", "tag": "geoip-$currentCountryCode", "format": "binary", - "path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir + "path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir }); ruleSets.add({ @@ -1360,19 +1414,25 @@ class KRSingBoxImp { }); 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'); + 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'); + 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", + "url": + "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs", "download_detour": "direct", "update_interval": "7d" }); @@ -1381,7 +1441,8 @@ class KRSingBoxImp { "type": "remote", "tag": "geosite-$currentCountryCode", "format": "binary", - "url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs", + "url": + "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs", "download_detour": "direct", "update_interval": "7d" }); @@ -1418,7 +1479,8 @@ class KRSingBoxImp { if (Platform.isWindows && !_dnsBackedUp) { KRLogUtil.kr_i('🪟 Windows 平台,首次启动备份 DNS 设置...', tag: 'SingBox'); try { - final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings(); + final backupSuccess = + await KRWindowsDnsUtil.instance.kr_backupDnsSettings(); if (backupSuccess) { _dnsBackedUp = true; // 标记已备份 KRLogUtil.kr_i('✅ Windows DNS 备份成功', tag: 'SingBox'); @@ -1449,11 +1511,11 @@ class KRSingBoxImp { final oOption = SingboxConfigOption.fromJson(_getConfigOption()); final changeResult = await kr_singBox.changeOptions(oOption).run(); changeResult.match( - (error) { + (error) { KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox'); throw Exception('初始化 HiddifyOptions 失败: $error'); }, - (_) { + (_) { KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox'); }, ); @@ -1463,7 +1525,9 @@ class KRSingBoxImp { if (await configFile.exists()) { final configContent = await configFile.readAsString(); KRLogUtil.kr_i('📄 配置文件内容长度: ${configContent.length}', tag: 'SingBox'); - KRLogUtil.kr_i('📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}', tag: 'SingBox'); + KRLogUtil.kr_i( + '📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}', + tag: 'SingBox'); } else { KRLogUtil.kr_w('⚠️ 配置文件不存在: $_cutPath', tag: 'SingBox'); } @@ -1472,7 +1536,7 @@ class KRSingBoxImp { _kr_subscribeToStatus(); await kr_singBox.start(_cutPath, kr_configName, false).map( - (r) { + (r) { KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox'); }, ).mapLeft((err) { @@ -1528,7 +1592,8 @@ class KRSingBoxImp { // 🔧 关键修复:恢复用户选择的节点 try { - final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); + final selectedNode = + await KRSecureStorage().kr_readData(key: _keySelectedNode); if (selectedNode != null && selectedNode.isNotEmpty) { KRLogUtil.kr_i('🎯 恢复用户选择的节点: $selectedNode', tag: 'SingBox'); if (kDebugMode) { @@ -1588,7 +1653,7 @@ class KRSingBoxImp { // 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间 try { await kr_singBox.stop().run().timeout( - const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒 + const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒 onTimeout: () { KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox'); return const Left('timeout'); @@ -1638,7 +1703,8 @@ class KRSingBoxImp { try { // 尝试恢复 DNS - final restoreSuccess = await KRWindowsDnsUtil.instance.kr_restoreDnsSettings(); + final restoreSuccess = + await KRWindowsDnsUtil.instance.kr_restoreDnsSettings(); if (restoreSuccess) { KRLogUtil.kr_i('✅ Windows DNS 恢复成功', tag: 'SingBox'); } else { @@ -1765,9 +1831,9 @@ class KRSingBoxImp { mode = KRCountryUtil.kr_getCurrentCountryCode(); KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox'); break; - // case KRConnectionType.direct: - // mode = "direct"; - // break; + // case KRConnectionType.direct: + // mode = "direct"; + // break; } oOption["region"] = mode; KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox'); @@ -1842,6 +1908,25 @@ class KRSingBoxImp { KRLogUtil.kr_i('🎯 [v2.1] 开始选择出站节点: $tag', tag: 'SingBox'); KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox'); + // 如果活动组尚未就绪,等待一段时间;仍为空则仅保存选择,稍后恢复 + if (kr_activeGroups.isEmpty) { + for (int i = 0; i < 20; i++) { + await Future.delayed(const Duration(milliseconds: 100)); + if (kr_activeGroups.isNotEmpty) break; + } + if (kr_activeGroups.isEmpty) { + KRLogUtil.kr_w('⚠️ 活动组为空,跳过即时切换,仅保存选择: $tag', tag: 'SingBox'); + try { + await KRSecureStorage() + .kr_saveData(key: _keySelectedNode, value: tag); + KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'SingBox'); + } catch (e) { + KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox'); + } + return; + } + } + // 🔧 诊断:打印所有活动组的节点,确保目标节点存在 KRLogUtil.kr_i('🔍 搜索目标节点 "$tag" 在活动组中...', tag: 'SingBox'); bool foundNode = false; @@ -1880,9 +1965,12 @@ class KRSingBoxImp { // 🔧 关键修复:使用正确的 group tag // libcore 生成的selector组的tag是"proxy"而不是"select" - 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); + 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'); // 🔧 新增:验证节点切换是否生效 @@ -1900,13 +1988,14 @@ class KRSingBoxImp { _nodeSelectionTimer?.cancel(); if (tag != 'auto') { KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox'); - _nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) { + _nodeSelectionTimer = + Timer.periodic(const Duration(seconds: 20), (timer) { // 每 20 秒重新选择一次,确保用户选择不被覆盖 // 使用 then/catchError 避免异常导致 UI 阻塞 kr_singBox.selectOutbound("select", tag).run().then((result) { result.match( - (error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'), - (_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'), + (error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'), + (_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'), ); }).catchError((error) { KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox'); @@ -1935,10 +2024,14 @@ class KRSingBoxImp { // 打印所有活动组信息 for (int i = 0; i < kr_activeGroups.length; i++) { final group = kr_activeGroups[i]; - KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox'); + KRLogUtil.kr_i( + '📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', + tag: 'SingBox'); for (int j = 0; j < group.items.length; j++) { final item = group.items[j]; - KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox'); + KRLogUtil.kr_i( + ' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', + tag: 'SingBox'); } } @@ -1954,10 +2047,14 @@ class KRSingBoxImp { KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox'); for (int i = 0; i < kr_activeGroups.length; i++) { final group = kr_activeGroups[i]; - KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox'); + KRLogUtil.kr_i( + '📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', + tag: 'SingBox'); for (int j = 0; j < group.items.length; j++) { final item = group.items[j]; - KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox'); + KRLogUtil.kr_i( + ' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', + tag: 'SingBox'); } } } catch (e) { diff --git a/lib/app/utils/kr_common_util.dart b/lib/app/utils/kr_common_util.dart index 976d638..b956ddb 100755 --- a/lib/app/utils/kr_common_util.dart +++ b/lib/app/utils/kr_common_util.dart @@ -5,7 +5,7 @@ class KRCommonUtil { /// 提示 meesage: 提示内容, toastPosition: 提示显示的位置, timeout: 显示时间(毫秒) static kr_showToast(String message, {KRToastPosition toastPosition = KRToastPosition.center, - int timeout = 1500}) { + int timeout = 2500}) { KRToast.kr_showToast( message, position: toastPosition, diff --git a/lib/app/widgets/hi_fixed_scrollbar.dart b/lib/app/widgets/hi_fixed_scrollbar.dart index fb3abaa..cb1254f 100644 --- a/lib/app/widgets/hi_fixed_scrollbar.dart +++ b/lib/app/widgets/hi_fixed_scrollbar.dart @@ -9,12 +9,16 @@ class HiFixedScrollbar extends StatefulWidget { /// 距离右边的间距(默认 18) final double right; + /// 滚动条宽度 final double thickness; + /// 滚动条颜色(默认白色 30%) final Color thumbColor; + /// 背景轨道颜色(默认白色 15%) final Color trackColor; + /// 滚动条固定高度(默认 50) final double thumbHeight; @@ -36,6 +40,7 @@ class HiFixedScrollbar extends StatefulWidget { class _HiFixedScrollbarState extends State { double _thumbOffset = 0.0; + bool _hasScrolled = false; @override void initState() { @@ -58,6 +63,7 @@ class _HiFixedScrollbarState extends State { setState(() { _thumbOffset = (trackHeight * scrollRatio).clamp(0, trackHeight); + _hasScrolled = offset > 0; }); } @@ -71,41 +77,44 @@ class _HiFixedScrollbarState extends State { Widget build(BuildContext context) { return LayoutBuilder( builder: (_, constraints) { + final showScrollbar = widget.isShowScrollbar && + widget.controller.hasClients && + widget.controller.position.maxScrollExtent > 0 && + _hasScrolled; return Stack( children: [ // 滚动内容 widget.child, - if(widget.isShowScrollbar) - ...[ - // 滚动条轨道 - Positioned( - right: widget.right.w, - top: 0, - bottom: 0, - child: Container( - width: widget.thickness.w, - decoration: BoxDecoration( - color: widget.trackColor, - borderRadius: BorderRadius.circular(4), - ), + if (showScrollbar) ...[ + // 滚动条轨道 + Positioned( + right: widget.right.w, + top: 0, + bottom: 0, + child: Container( + width: widget.thickness.w, + decoration: BoxDecoration( + color: widget.trackColor, + borderRadius: BorderRadius.circular(4), ), ), + ), - // 滚动条拇指 - Positioned( - right: widget.right.w, - top: _thumbOffset, - child: Container( - width: widget.thickness.w, - height: widget.thumbHeight, - decoration: BoxDecoration( - color: widget.thumbColor, - borderRadius: BorderRadius.circular(4), - ), + // 滚动条拇指 + Positioned( + right: widget.right.w, + top: _thumbOffset, + child: Container( + width: widget.thickness.w, + height: widget.thumbHeight, + decoration: BoxDecoration( + color: widget.thumbColor, + borderRadius: BorderRadius.circular(4), ), ), - ] + ), + ] ], ); }, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8c33c93..dbe4306 100755 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,6 +12,7 @@ import flutter_udid import package_info_plus import path_provider_foundation import screen_retriever_macos +import share_plus import tray_manager import url_launcher_macos import webview_flutter_wkwebview @@ -25,6 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) diff --git a/pubspec.lock b/pubspec.lock index b3c1fd8..2eda2e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -254,6 +254,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: "direct main" description: @@ -1049,10 +1057,10 @@ packages: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.6" nm: dependency: transitive description: @@ -1413,6 +1421,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + url: "https://pub.dev" + source: hosted + version: "7.2.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" + url: "https://pub.dev" + source: hosted + version: "3.4.0" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a95c0a6..548722b 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: flutter_inappwebview: ^6.1.5 # 最新稳定版本 crisp_sdk: ^1.1.0 # 使用 crisp_sdk,配合最新的 flutter_inappwebview protocol_handler_windows: ^0.2.0 + share_plus: ^7.2.2 # 国际化 slang: ^3.30.1 @@ -302,4 +303,3 @@ ffigen: headers: entry-points: - "libcore/bin/libcore.h" - diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 249d6e0..e8fc12e 100755 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); TrayManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1101ef7..2363a7a 100755 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows protocol_handler_windows screen_retriever_windows + share_plus tray_manager url_launcher_windows window_manager