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 5f6a2f3..b738dad 100755 --- a/lib/app/modules/kr_home/controllers/kr_home_controller.dart +++ b/lib/app/modules/kr_home/controllers/kr_home_controller.dart @@ -90,6 +90,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 是否显示延迟 final kr_isLatency = false.obs; + final kr_isPositionAnimating = false.obs; + /// 默认 var kr_cutTag = ''.obs; var kr_cutSeletedTag = ''.obs; @@ -217,14 +219,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 ?? ''}', + 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}', + 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}', + KRLogUtil.kr_i( + '准备后 kr_cutSeletedTag_autoConnect: ${kr_cutSeletedTag.value}', tag: 'HomeController'); await kr_performNodeSwitch(selectedAfter!); await KRSingBoxImp.instance.kr_start(); @@ -315,11 +320,42 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { }); // 🔧 Android 15 新增:5秒后再次强制更新高度,兜底保护 - Future.delayed(const Duration(seconds: 5), () { - if (kr_bottomPanelHeight.value < 100) { - KRLogUtil.kr_w('检测到底部面板高度异常(${kr_bottomPanelHeight.value}),强制修正', - tag: 'HomeController'); - kr_updateBottomPanelHeight(); + // Future.delayed(const Duration(seconds: 5), () { + // if (kr_bottomPanelHeight.value < 100) { + // KRLogUtil.kr_w('检测到底部面板高度异常(${kr_bottomPanelHeight.value}),强制修正', + // tag: 'HomeController'); + // kr_updateBottomPanelHeight(); + // } + // }); + + _initShowAnimationWatcher(); + } + + bool _lastIsShow = false; + + void _initShowAnimationWatcher() { + _lastIsShow = kr_currentNodeLatency.value == -1 || kr_isConnected.value; + everAll([kr_currentNodeLatency, kr_isConnected], (_) { + final v = kr_currentNodeLatency.value == -1 || kr_isConnected.value; + if (v != _lastIsShow) { + _triggerPositionAnim(); + _lastIsShow = v; + } + }); + } + + void kr_onPositionAnimationEnd() { + kr_isPositionAnimating.value = false; + } + + int _positionAnimToken = 0; + void _triggerPositionAnim() { + _positionAnimToken++; + final token = _positionAnimToken; + kr_isPositionAnimating.value = true; + Future.delayed(const Duration(milliseconds: 450), () { + if (_positionAnimToken == token) { + kr_isPositionAnimating.value = false; } }); } @@ -938,6 +974,26 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { if (kDebugMode) {} } + DateTime? _lastConnectClickAt; + static const Duration _connectClickCooldown = Duration(milliseconds: 600); + + bool kr_canProcessConnectClick() { + final now = DateTime.now(); + if (kr_isPositionAnimating.value) { + return false; + } + final st = KRSingBoxImp.instance.kr_status.value; + if (st is SingboxStarting || st is SingboxStopping) { + return false; + } + if (_lastConnectClickAt != null && + now.difference(_lastConnectClickAt!) < _connectClickCooldown) { + return false; + } + _lastConnectClickAt = now; + return true; + } + /// 🔧 等待状态达到预期值 Future _waitForStatus(Type expectedType, {int maxSeconds = 3}) async { if (kDebugMode) {} @@ -1148,7 +1204,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { /// 处理手动模式 void _kr_handleManualMode(dynamic element) { try { - KRLogUtil.kr_d('处理手动模式 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}', tag: 'HomeController'); + KRLogUtil.kr_d( + '处理手动模式 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}', + tag: 'HomeController'); // 🔧 关键修复:不要用内核返回的 selected 覆盖用户手动选择 // 只有当内核返回的节点与用户选择一致时,才更新相关状态 @@ -1160,12 +1218,15 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 只有当内核确认切换成功时,才更新 UI 状态 if (element.selected == kr_cutTag.value) { kr_cutSeletedTag.value = element.selected; - kr_currentNodeName.value = kr_truncateText(element.selected, maxLength: 25); + kr_currentNodeName.value = + kr_truncateText(element.selected, maxLength: 25); // kr_moveToSelectedNode(); - KRLogUtil.kr_d('✅ 内核确认节点切换成功: ${element.selected}', tag: 'HomeController'); + KRLogUtil.kr_d('✅ 内核确认节点切换成功: ${element.selected}', + tag: 'HomeController'); } else { // 内核返回的节点与用户选择不一致,保持用户选择的显示 - KRLogUtil.kr_d('⏳ 等待内核切换到用户选择的节点: ${kr_cutTag.value}', tag: 'HomeController'); + KRLogUtil.kr_d('⏳ 等待内核切换到用户选择的节点: ${kr_cutTag.value}', + tag: 'HomeController'); } } catch (e) { KRLogUtil.kr_e('处理手动模式出错: $e', tag: 'HomeController'); @@ -1364,9 +1425,12 @@ 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; } @@ -1393,7 +1457,9 @@ 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'); @@ -1412,11 +1478,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 🔧 保存新节点选择 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); // 🚀 核心改进:使用 selectOutbound 进行热切换(参考 hiddify-app) // 优势:不重启VPN,保持连接状态,切换瞬间完成,VPN开关不闪烁 - KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...', tag: 'HomeController'); + KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...', + tag: 'HomeController'); // 🔧 关键修复:确定正确的 selector 组 tag // selectOutbound(groupTag, outboundTag) - 第一个参数是组的tag,不是节点的tag @@ -1427,12 +1495,14 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { for (var group in activeGroups) { if (group.type == ProxyType.selector) { selectorGroupTag = group.tag; - KRLogUtil.kr_i('🔍 找到 selector 组: $selectorGroupTag', tag: 'HomeController'); + KRLogUtil.kr_i('🔍 找到 selector 组: $selectorGroupTag', + tag: 'HomeController'); break; } } - KRLogUtil.kr_i('📡 调用 selectOutbound("$selectorGroupTag", "$tag")', tag: 'HomeController'); + KRLogUtil.kr_i('📡 调用 selectOutbound("$selectorGroupTag", "$tag")', + tag: 'HomeController'); // 调用 sing-box 的 selectOutbound API final result = await KRSingBoxImp.instance.kr_singBox @@ -1441,12 +1511,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 处理切换结果 result.fold( - (error) { + (error) { // 切换失败 - KRLogUtil.kr_e('❌ selectOutbound 调用失败: $error', tag: 'HomeController'); + KRLogUtil.kr_e('❌ selectOutbound 调用失败: $error', + tag: 'HomeController'); throw Exception('节点切换失败: $error'); }, - (_) { + (_) { // 切换成功 KRLogUtil.kr_i('✅ selectOutbound 调用成功', tag: 'HomeController'); }, @@ -1466,15 +1537,18 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { try { final updatedGroups = KRSingBoxImp.instance.kr_activeGroups; final selectGroup = updatedGroups.firstWhere( - (group) => group.type == ProxyType.selector, + (group) => group.type == ProxyType.selector, orElse: () => throw Exception('未找到 selector 组'), ); - KRLogUtil.kr_i('📊 [验证] ${selectGroup.tag}组当前选中: ${selectGroup.selected}', 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'); @@ -1488,7 +1562,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { KRLogUtil.kr_i('✅ 节点热切换成功,VPN保持连接: $tag', tag: 'HomeController'); return true; - } catch (switchError) { // 后台切换失败,恢复到原节点 KRLogUtil.kr_e('❌ 后台节点切换失败: $switchError', tag: 'HomeController'); @@ -1500,7 +1573,8 @@ 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_animated_connect_button.dart b/lib/app/modules/kr_home/views/hi_animated_connect_button.dart index b12e2a8..f398659 100644 --- a/lib/app/modules/kr_home/views/hi_animated_connect_button.dart +++ b/lib/app/modules/kr_home/views/hi_animated_connect_button.dart @@ -22,113 +22,96 @@ class HIAnimatedConnectButton extends GetView { @override Widget build(BuildContext context) { return Obx(() { - final delay = controller.kr_currentNodeLatency.value; + final _ = KRSingBoxImp.instance.kr_status.value; - // 🔧 关键: 强制读取两个 observable 确保追踪 - final _ = KRSingBoxImp.instance.kr_status.value; // 强制追踪 - final isConnected = controller.kr_isConnected.value; // 使用 controller 的状态 - - // 再次读取状态用于判断 - final status = KRSingBoxImp.instance.kr_status.value; - final isSwitching = status is SingboxStarting || status is SingboxStopping; - - print('🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching'); - - final isShow = delay == -1 || isConnected; - - final Color buttonColor = Theme.of(context).primaryColor; final double screenWidth = Get.width; final double screenHeight = Get.height; final double buttonSize = 340; - final double currentButtonSize = isShow ? 270 : buttonSize; + final double currentButtonSize = 270; return Stack( children: [ AnimatedPositioned( duration: const Duration(milliseconds: 400), curve: Curves.linear, - left: isShow ? (screenWidth - currentButtonSize) / 2 : -60, - bottom: isShow ? screenHeight * 0.2 : -40, + left: (screenWidth - currentButtonSize) / 2, + bottom: screenHeight * 0.2, + onEnd: () { + controller.kr_onPositionAnimationEnd(); + }, child: Stack( alignment: Alignment.center, clipBehavior: Clip.none, children: [ - /// ✅ 呼吸同心圆背景 - if (isShow) + if (controller.kr_isPositionAnimating.value) Positioned.fill( child: OverflowBox( maxWidth: buttonSize * 3, maxHeight: buttonSize * 3, child: ContinuousRippleEffect( - color: Theme.of(context).primaryColor - ), + color: Theme.of(context).primaryColor), ), ), - - if (!isShow) - Positioned( - top: -20, // 距离顶部 10.w - left: 115, // 距离右侧 10.w - child: KrLocalImage( - imageName: "hi-home-slogan", - imageType: ImageType.svg, - ), - ), - /// ✅ 按钮主体 Material( shape: const CircleBorder(), - color: isShow ? Colors.transparent : Theme.of(context).primaryColor, + color: Colors.transparent, clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () { - if(isSwitching) { - print('🔵 Switch UI 正在更新,切换中点击了按钮: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching'); - return; - } - final current = controller.kr_subscribeService.kr_currentSubscribe.value; - bool hasValidSubscription = false; - if (current != null) { - DateTime? expire; - try { - expire = DateTime.parse(current.expireTime); - } catch (_) { - expire = null; + child: AbsorbPointer( + absorbing: controller.kr_isPositionAnimating.value, + child: InkWell( + onTap: () { + if (!controller.kr_canProcessConnectClick()) { + return; } - hasValidSubscription = expire != null && expire.isAfter(DateTime.now()); - } - if (hasValidSubscription) { - controller.kr_toggleSwitch(!controller.kr_isConnected.value); - } else { - HIDialog.show( - customMessageWidget: Padding( - padding: EdgeInsets.only(top: 16.w), - child: Text( - '尚未购买套餐,请前往购买页面下单后即可开始极速网络体验', - textAlign: TextAlign.left, - style: KrAppTextStyle( - color: Theme.of(context).textTheme.bodyMedium?.color, - fontSize: 14.sp, - fontWeight: FontWeight.w600, + final current = controller + .kr_subscribeService.kr_currentSubscribe.value; + bool hasValidSubscription = false; + if (current != null) { + DateTime? expire; + try { + expire = DateTime.parse(current.expireTime); + } catch (_) { + expire = null; + } + hasValidSubscription = + expire != null && expire.isAfter(DateTime.now()); + } + if (hasValidSubscription) { + controller.kr_toggleSwitch( + !controller.kr_isConnected.value); + } else { + HIDialog.show( + customMessageWidget: Padding( + padding: EdgeInsets.only(top: 16.w), + child: Text( + '尚未购买套餐,请前往购买页面下单后即可开始极速网络体验', + textAlign: TextAlign.left, + style: KrAppTextStyle( + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), ), ), - ), - cancelText: '取消', - confirmText: '前往', - onConfirm: () { - GlobalOverlayService.instance.triggerSubscriptionAnimation(); - }, - ); - } - }, - child: SizedBox( - width: currentButtonSize, - height: currentButtonSize, - child: Stack( - alignment: Alignment.center, - children: [ - if (isShow) - /// ✅ isShow = true,图片居中,文字贴近底部 + cancelText: '取消', + confirmText: '前往', + onConfirm: () { + GlobalOverlayService.instance + .triggerSubscriptionAnimation(); + }, + ); + } + }, + child: SizedBox( + width: currentButtonSize, + height: currentButtonSize, + child: Stack( + alignment: Alignment.center, + children: [ Stack( alignment: Alignment.center, children: [ @@ -148,52 +131,25 @@ class HIAnimatedConnectButton extends GetView { controller.kr_connectText.value == '已连接' ? '已连接\n点击断开' : controller.kr_connectText.value, - key: ValueKey(controller.kr_connectText.value), + key: ValueKey( + controller.kr_connectText.value), textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold, - height: 1 + height: 1, ), ), ), ], - ) - else - /// ✅ isShow = false,恢复原本布局 - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - KrLocalImage( - imageName: "hi-home-logo", - width: 104, - imageType: ImageType.svg, - ), - SizedBox(height: 2.h), - Text( - controller.kr_connectText.value == '已连接' - ? '已连接\n点击断开' - : controller.kr_connectText.value, - key: ValueKey(controller.kr_connectText.value), - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - fontSize: 18.sp, - fontWeight: FontWeight.bold, - ), - ), - ], ), - ], + ], + ), ), ), - - ), ), - ], ), ), @@ -202,6 +158,7 @@ class HIAnimatedConnectButton extends GetView { }); } } + /// ✅ 呼吸式同心圆动画(最小圆固定270,其他圆在270~479间波动,初始直径差10%) /// ✅ 呼吸式同心圆动画(所有圆在270~470之间运动,起点相同但周期不同) class ContinuousRippleEffect extends StatefulWidget { @@ -232,10 +189,11 @@ class _ContinuousRippleEffectState extends State // 动画周期差异:外层慢,内层快 final duration = Duration(milliseconds: 3000 + i * 1200); - final controller = - AnimationController(vsync: this, duration: duration)..repeat(reverse: true); + final controller = AnimationController(vsync: this, duration: duration) + ..repeat(reverse: true); - final curved = CurvedAnimation(parent: controller, curve: Curves.easeInOutSine); + final curved = + CurvedAnimation(parent: controller, curve: Curves.easeInOutSine); _controllers.add(controller); _animations.add(curved); }