feat: bugfix

This commit is contained in:
2025-12-01 06:54:18 -08:00
parent 1b1b38aba5
commit 86687cac58
22 changed files with 827 additions and 493 deletions
@@ -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<KRDeleteAccountController> {
@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<KRDeleteAccountController> {
),
),
),*/
SizedBox(height: 20.w),
],
SizedBox(height: 20.w),
],
),
),
),
),
@@ -129,11 +136,9 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
);
}),
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<KRDeleteAccountController> {
/// 构建注册页面的验证码输入框(包含间距)
Widget _buildVerificationCodeField() {
return SizedBox(
height: 50.w, // 固定高度
height: 50.w,
child: TextField(
controller: controller.kr_codeController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.oneTimeCode],
enableSuggestions: false,
autocorrect: false,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) {
var v = value.replaceAll(RegExp(r"\s+"), "");
if (v.length % 2 == 0 && v.isNotEmpty) {
final half = v.length ~/ 2;
final first = v.substring(0, half);
final second = v.substring(half);
if (first == second) {
v = first;
}
}
const maxLen = 6;
if (v.length > maxLen) {
v = v.substring(0, maxLen);
}
if (controller.kr_codeController.text != v) {
controller.kr_codeController.value =
controller.kr_codeController.value.copyWith(
text: v,
selection: TextSelection.collapsed(offset: v.length),
composing: TextRange.empty,
);
}
if (v.isNotEmpty && (v.length >= 6)) {
FocusScope.of(Get.context!).unfocus();
}
},
onSubmitted: (_) {
FocusScope.of(Get.context!).unfocus();
},
style: KrAppTextStyle(
fontSize: 16,
color: Colors.white,
@@ -184,19 +222,20 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
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<KRDeleteAccountController> {
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
@@ -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<dynamic> 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');
}
@@ -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<HISubscriptionCornerButton>
child: KRPurchaseMembershipView(),
startOffset: startOffset,
transitionColor: transitionColor,
customDuration: const Duration(milliseconds: 1500),
customDuration: const Duration(milliseconds: 500),
),
).then((_) {
// 页面返回后恢复默认主题色
@@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import '../controllers/kr_invite_controller.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
@@ -15,7 +16,6 @@ class KRInviteView extends GetView<KRInviteController> {
@override
Widget build(BuildContext context) {
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
return HIBaseScaffold(
@@ -35,7 +35,8 @@ class KRInviteView extends GetView<KRInviteController> {
// 🟢 第一行:奖励说明
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<KRInviteController> {
SizedBox(height: 26.w),
// 🟢 第二行:我的邀请码
Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
padding:
EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2.0),
borderRadius: BorderRadius.circular(1000.r),
),
child: Obx(
() => Row(
() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
@@ -113,12 +115,27 @@ class KRInviteView extends GetView<KRInviteController> {
),
onPressed: () {
if (controller.kr_referCode.value.isNotEmpty) {
Clipboard.setData(
ClipboardData(text: controller.kr_referCode.value),
);
KRCommonUtil.kr_showToast(
AppTranslations.kr_invite.inviteCodeCopied,
);
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<KRInviteController> {
TextField(
controller: controller.otherInviteCodeController,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
decoration: InputDecoration(
hintText: '填入邀请人邀请码兑换免费时长...',
hintStyle: const TextStyle(color: Color(0xFFA6A6A6)),
filled: true,
fillColor: Colors.transparent,
contentPadding: EdgeInsets.symmetric(horizontal: 22.w),
contentPadding:
EdgeInsets.symmetric(horizontal: 22.w),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
borderSide: const BorderSide(
color: Colors.white, width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
borderSide: const BorderSide(
color: Colors.white, width: 2.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
borderSide: const BorderSide(
color: Colors.white, width: 2.0),
),
constraints: BoxConstraints(maxHeight: 50.h),
),
@@ -199,8 +221,7 @@ class KRInviteView extends GetView<KRInviteController> {
),
// 5. 将 HIHelpEntrance 作为 Stack 的直接子元素,它将恢复其原有的绝对定位能力
if (!isKeyboardVisible)
const HIHelpEntrance(),
if (!isKeyboardVisible) const HIHelpEntrance(),
],
),
);
@@ -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<KRLoginController> {
const KRLoginView({super.key});
@@ -27,23 +28,29 @@ class KRLoginView extends GetView<KRLoginController> {
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<KRLoginController> {
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<KRLoginController> {
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,
@@ -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<KRMessageController> {
const KRMessageView({super.key});
@@ -25,62 +26,77 @@ class KRMessageView extends GetView<KRMessageController> {
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(),
],
@@ -25,13 +25,10 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
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),
@@ -92,14 +92,14 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
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<KRPurchaseMembershipController> {
int planIndex,
int? discountIndex,
BuildContext context,
bool isFirst,
int index,
) {
// 由于移除了 ObxisSelected 的值在构建时就固定了。
// 它将不再响应用户的点击事件来改变UI。
// 根据您的要求,isSelected 的值与 isFirst 相反。
bool isFirst = 0 == index;
bool isSelected = !isFirst;
return GestureDetector(
@@ -190,16 +191,16 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
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<KRPurchaseMembershipController> {
: 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<KRPurchaseMembershipController> {
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<KRPurchaseMembershipController> {
style: TextStyle(
color: Colors.black, // 白色文字更清晰
fontSize: 14,
fontWeight: FontWeight.w300,
fontWeight: index == 1
? FontWeight.w300
: FontWeight.w600,
),
textAlign: TextAlign.center,
),
@@ -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
@@ -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;
+3 -9
View File
@@ -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,
),
),
),
+258 -161
View File
@@ -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<void> _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<String, dynamic>;
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<Map<String, dynamic>> _kr_buildHiddifyRules() {
final rules = <Map<String, dynamic>>[];
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<String, dynamic> 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<void> _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<void> _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<String, dynamic> 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 = <Map<String, dynamic>>[];
// 基础规则: 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) {
+1 -1
View File
@@ -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,
+34 -25
View File
@@ -9,12 +9,16 @@ class HiFixedScrollbar extends StatefulWidget {
/// 距离右边的间距(默认 18
final double right;
/// 滚动条宽度
final double thickness;
/// 滚动条颜色(默认白色 30%)
final Color thumbColor;
/// 背景轨道颜色(默认白色 15%)
final Color trackColor;
/// 滚动条固定高度(默认 50
final double thumbHeight;
@@ -36,6 +40,7 @@ class HiFixedScrollbar extends StatefulWidget {
class _HiFixedScrollbarState extends State<HiFixedScrollbar> {
double _thumbOffset = 0.0;
bool _hasScrolled = false;
@override
void initState() {
@@ -58,6 +63,7 @@ class _HiFixedScrollbarState extends State<HiFixedScrollbar> {
setState(() {
_thumbOffset = (trackHeight * scrollRatio).clamp(0, trackHeight);
_hasScrolled = offset > 0;
});
}
@@ -71,41 +77,44 @@ class _HiFixedScrollbarState extends State<HiFixedScrollbar> {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
final showScrollbar = widget.isShowScrollbar &&
widget.controller.hasClients &&
widget.controller.position.maxScrollExtent > 0 &&
_hasScrolled;
return Stack(
children: [
// 滚动内容
widget.child,
if(widget.isShowScrollbar)
...[
// 滚动条轨道
Positioned(
right: widget.right.w,
top: 0,
bottom: 0,
child: Container(
width: widget.thickness.w,
decoration: BoxDecoration(
color: widget.trackColor,
borderRadius: BorderRadius.circular(4),
),
if (showScrollbar) ...[
// 滚动条轨道
Positioned(
right: widget.right.w,
top: 0,
bottom: 0,
child: Container(
width: widget.thickness.w,
decoration: BoxDecoration(
color: widget.trackColor,
borderRadius: BorderRadius.circular(4),
),
),
),
// 滚动条拇指
Positioned(
right: widget.right.w,
top: _thumbOffset,
child: Container(
width: widget.thickness.w,
height: widget.thumbHeight,
decoration: BoxDecoration(
color: widget.thumbColor,
borderRadius: BorderRadius.circular(4),
),
// 滚动条拇指
Positioned(
right: widget.right.w,
top: _thumbOffset,
child: Container(
width: widget.thickness.w,
height: widget.thumbHeight,
decoration: BoxDecoration(
color: widget.thumbColor,
borderRadius: BorderRadius.circular(4),
),
),
]
),
]
],
);
},