feat: bugfix
This commit is contained in:
@@ -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,
|
||||
) {
|
||||
// 由于移除了 Obx,isSelected 的值在构建时就固定了。
|
||||
// 它将不再响应用户的点击事件来改变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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user