fix: 更新诸多bug

This commit is contained in:
2026-01-08 02:01:21 -08:00
parent 9563a81a6e
commit 8477c2e343
96 changed files with 1338 additions and 5695 deletions
+10 -16
View File
@@ -3,6 +3,7 @@ import '../services/api_service/kr_api.user.dart';
import '../utils/kr_update_util.dart';
import '../utils/kr_secure_storage.dart';
import '../utils/kr_log_util.dart';
import '../utils/kr_http_adapter_util.dart';
import '../services/singbox_imp/kr_sing_box_imp.dart';
import '../utils/kr_init_log_collector.dart'; // 🔧 新增:导入日志收集器
import 'dart:async';
@@ -31,7 +32,7 @@ class KRDomain {
// static String kr_currentDomain = "apicn.bearvpn.top";
static List<String> kr_baseDomains = ["api.hifast.biz", "api.airovpn.tel",];
static String kr_currentDomain = "api.hifast.biz1";
static String kr_currentDomain = "api.hifast.biz";
// 备用域名获取地址列表
static List<String> kr_backupDomainUrls = [
@@ -67,21 +68,14 @@ class KRDomain {
// Dio 实例及初始化
static final Dio _dio = (() {
final dio = Dio();
// 🔧 配置HttpClientAdapter使用sing-box的mixed代理
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.findProxy = (url) {
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
KRLogUtil.kr_i(
'🔍 KRDomain 请求使用代理: $proxyConfig, url: $url',
tag: 'KRDomain',
);
return proxyConfig;
};
return client;
},
final dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: kr_domainTimeout),
sendTimeout: const Duration(seconds: kr_domainTimeout),
receiveTimeout: const Duration(seconds: kr_domainTimeout),
));
// 🔧 使用统一的 Adapter 转换工具
dio.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
timeout: const Duration(seconds: kr_domainTimeout),
);
return dio;
})();
+5
View File
@@ -44,6 +44,9 @@ class KRAppRunData {
/// 用户邀请码(从用户信息接口获取)
final RxString kr_referCode = ''.obs;
/// 邀请人ID(谁邀请了当前用户)
final RxInt kr_refererId = 0.obs;
/// 用户余额
final RxInt kr_balance = 0.obs;
@@ -597,12 +600,14 @@ class KRAppRunData {
// 保存到全局状态
kr_referCode.value = userInfo.referCode;
kr_refererId.value = userInfo.refererId;
kr_account.value = authType == 'device' ? '9000${userInfo.id}' : authIdentifier;
kr_balance.value = userInfo.balance;
kr_commission.value = userInfo.commission;
KRLogUtil.kr_i('💾 [AppRunData] 用户信息已保存到全局状态:', tag: 'AppRunData');
KRLogUtil.kr_i(' - kr_referCode: "${kr_referCode.value}"', tag: 'AppRunData');
KRLogUtil.kr_i(' - kr_refererId: ${kr_refererId.value}', tag: 'AppRunData');
KRLogUtil.kr_i(' - kr_balance: ${kr_balance.value}', tag: 'AppRunData');
KRLogUtil.kr_i(' - kr_commission: ${kr_commission.value}', tag: 'AppRunData');
},
@@ -1,12 +0,0 @@
import 'package:get/get.dart';
import '../controllers/kr_country_selector_controller.dart';
class KRCountrySelectorBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRCountrySelectorController>(
() => KRCountrySelectorController(),
);
}
}
@@ -1,43 +0,0 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import '../../../services/singbox_imp/kr_sing_box_imp.dart';
class KRCountrySelectorController extends GetxController {
// 使用 KRCountry 枚举来加载国家
final RxList<KRCountry> kr_countries = <KRCountry>[].obs;
// 当前选中的国家
final Rx<KRCountry> kr_selectedCountry = KRCountry.cn.obs;
@override
void onInit() {
super.onInit();
kr_selectedCountry.value = KRCountryUtil.kr_currentCountry.value;
kr_loadCountries();
}
// 加载国家数据
void kr_loadCountries() {
kr_countries.value = KRCountryUtil.kr_getSupportedCountries();
}
// 选择国家
Future<void> kr_selectCountry(KRCountry country) async {
kr_selectedCountry.value = country;
// try {
// await KRSingBoxImp().kr_updateCountry(country);
// // Get.back();
// } catch (err) {
// }
}
@override
void onClose() {
// TODO: implement onClose
super.onClose();
KRSingBoxImp().kr_updateCountry(kr_selectedCountry.value);
}
}
@@ -1,116 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import '../controllers/kr_country_selector_controller.dart';
class KRCountrySelectorView extends GetView<KRCountrySelectorController> {
const KRCountrySelectorView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
// 非渐变色区域
],
stops: [0.0, 0.28], // 调整渐变结束位置
),
),
child: Column(
children: [
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_setting.countrySelector,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => ListView.separated(
padding: EdgeInsets.all(16.r),
itemCount: controller.kr_countries.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final country = controller.kr_countries[index];
return _kr_buildCountryCard(country, context);
},
),
),
),
],
),
),
);
}
// 构建国家卡片
Widget _kr_buildCountryCard(KRCountry country, BuildContext context) {
return Obx(
() => InkWell(
onTap: () => controller.kr_selectCountry(country),
child: Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
// 国家图标
KRCountryFlag(
countryCode: country.kr_code,
width: 24.r,
height: 24.r,
),
SizedBox(width: 12.w),
// 国家名称
Text(
KRCountryUtil.kr_getCountryName(country),
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
// 选中标记
if (controller.kr_selectedCountry.value == country)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.r,
),
],
),
),
),
);
}
}
@@ -1,12 +0,0 @@
import 'package:get/get.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRDeviceManagementController>(
() => KRDeviceManagementController(),
);
}
}
@@ -1,294 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_api.user.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import 'package:kaer_with_panels/app/services/kr_device_info_service.dart';
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'dart:io';
import 'dart:math';
class KRDeviceManagementController extends GetxController {
// 设备列表
final RxList<Map<String, dynamic>> devices = <Map<String, dynamic>>[].obs;
// 加载状态
final RxBool isLoading = true.obs;
// 当前设备ID
String? currentDeviceId;
@override
void onInit() {
super.onInit();
_initDeviceId();
loadDeviceList();
}
/// 初始化当前设备ID
Future<void> _initDeviceId() async {
try {
currentDeviceId = KRDeviceInfoService().deviceId ?? 'unknown';
KRLogUtil.kr_i('当前设备ID: $currentDeviceId', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('获取设备ID失败: $e', tag: 'DeviceManagement');
}
}
/// 加载设备列表
Future<void> loadDeviceList() async {
try {
isLoading.value = true;
KRLogUtil.kr_i('开始加载设备列表', tag: 'DeviceManagement');
// 调用API获取设备列表
final result = await KRUserApi().kr_getUserDevices();
result.fold(
(error) {
KRLogUtil.kr_e('加载设备列表失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, error.msg);
},
(deviceList) {
KRLogUtil.kr_i('获取到 ${deviceList.length} 个设备', tag: 'DeviceManagement');
// 转换设备数据格式
devices.value = deviceList.map((device) {
final identifier = device['identifier']?.toString() ?? '';
final isCurrent = identifier == currentDeviceId;
return {
'id': device['id']?.toString() ?? '',
'identifier': identifier,
'device_name': device['user_agent'] ?? '未知设备',
'ip': device['ip'] ?? '',
'last_login': device['updated_at'] ?? device['created_at'] ?? '',
'is_current': isCurrent,
'enabled': device['enabled'] ?? true,
'online': device['online'] ?? false,
};
}).toList();
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('加载设备列表异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.loadDeviceListFailed);
} finally {
isLoading.value = false;
}
}
/// 删除设备
Future<void> deleteDevice(String id) async {
try {
// 检查是否是本机设备
final device = devices.firstWhere(
(d) => d['id'] == id,
orElse: () => {},
);
if (device.isEmpty) return;
final isCurrent = device['is_current'] ?? false;
// 使用响应式变量来接收确认结果
bool? confirmed;
// 显示确认对话框
await KRDialog.show(
title: AppTranslations.kr_deviceManagement.deleteConfirmTitle,
message: isCurrent
? AppTranslations.kr_deviceManagement.deleteCurrentDeviceMessage
: AppTranslations.kr_deviceManagement.deleteOtherDeviceMessage,
icon: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_rounded,
color: Colors.red,
size: 32,
),
),
confirmText: AppTranslations.kr_dialog.delete,
cancelText: AppTranslations.kr_dialog.kr_cancel,
onConfirm: () {
confirmed = true;
},
onCancel: () {
confirmed = false;
},
);
if (confirmed != true) return;
KRLogUtil.kr_i('开始解绑设备 - id: $id, isCurrent: $isCurrent', tag: 'DeviceManagement');
// 调用API解绑设备
final result = await KRUserApi().kr_unbindUserDevice(id);
result.fold(
(error) {
KRLogUtil.kr_e('删除设备失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(error.msg));
},
(_) async {
KRLogUtil.kr_i('设备删除成功', tag: 'DeviceManagement');
if (isCurrent) {
// 如果删除的是本机设备,重新进行设备登录
KRLogUtil.kr_i('本机设备已删除,准备重新登录', tag: 'DeviceManagement');
// 先关闭当前设备管理页面
Get.back();
// 执行重新登录
await _reloginWithDevice();
} else {
// 删除其他设备,从列表中移除
devices.removeWhere((device) => device['id'] == id);
Get.snackbar(AppTranslations.kr_dialog.success, AppTranslations.kr_deviceManagement.deleteSuccess);
}
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('删除设备异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(e.toString()));
}
}
/// 重新使用设备登录
Future<void> _reloginWithDevice() async {
try {
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
KRLogUtil.kr_i('开始重新进行设备登录', tag: 'DeviceManagement');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
// 先清除当前的用户信息(但不调用 kr_loginOut,避免显示登录界面)
final appRunData = KRAppRunData.getInstance();
appRunData.kr_isLogin.value = false;
appRunData.kr_token = null;
appRunData.kr_account.value = null;
appRunData.kr_userId.value = null;
// 检查是否启用设备登录
final siteConfigService = KRSiteConfigService();
final isDeviceLoginEnabled = siteConfigService.isDeviceLoginEnabled();
if (!isDeviceLoginEnabled) {
KRLogUtil.kr_w('设备登录未启用,执行完整退出登录', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.tip, AppTranslations.kr_deviceManagement.deviceLoginDisabled);
await appRunData.kr_loginOut();
return;
}
KRLogUtil.kr_i('设备登录已启用,开始调用设备登录接口', tag: 'DeviceManagement');
// 初始化设备信息服务(如果还没初始化)
await KRDeviceInfoService().initialize();
// 调用设备登录接口
final authApi = KRAuthApi();
final result = await authApi.kr_deviceLogin();
result.fold(
(error) {
// 设备登录失败
KRLogUtil.kr_e('设备登录失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.reloginFailed(error.msg));
// 执行完整退出登录,显示登录界面
appRunData.kr_loginOut();
},
(token) async {
// 设备登录成功
KRLogUtil.kr_i('✅ 设备登录成功!', tag: 'DeviceManagement');
KRLogUtil.kr_i('🎫 Token: ${token.substring(0, min(20, token.length))}...', tag: 'DeviceManagement');
// 保存新的用户信息
final deviceId = KRDeviceInfoService().deviceId ?? 'unknown';
await appRunData.kr_saveUserInfo(
token,
'device_$deviceId',
KRLoginType.kr_email,
null,
);
KRLogUtil.kr_i('✅ 设备重新登录成功,已更新用户信息', tag: 'DeviceManagement');
// 等待一小段时间,确保登录状态已经更新
await Future.delayed(const Duration(milliseconds: 300));
// 刷新订阅信息
KRLogUtil.kr_i('🔄 开始刷新订阅信息...', tag: 'DeviceManagement');
try {
await KRSubscribeService().kr_refreshAll();
KRLogUtil.kr_i('✅ 订阅信息刷新成功', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('订阅信息刷新失败: $e', tag: 'DeviceManagement');
}
Get.snackbar(AppTranslations.kr_dialog.success, AppTranslations.kr_deviceManagement.reloginSuccess);
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('设备重新登录异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.reloginFailedGeneric);
// 发生异常,执行完整退出登录
await KRAppRunData.getInstance().kr_loginOut();
}
}
/// 获取设备类型和图标
Map<String, dynamic> getDeviceTypeInfo(String userAgent) {
String deviceType = AppTranslations.kr_deviceManagement.deviceTypeUnknown;
String iconName = 'devices';
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeAndroid;
iconName = 'phone_android';
} else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIos;
iconName = 'phone_iphone';
} else if (userAgent.contains('iPad')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIpad;
iconName = 'tablet';
} else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeMacos;
iconName = 'desktop_mac';
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeWindows;
iconName = 'computer';
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeLinux;
iconName = 'computer';
}
return {
'type': deviceType,
'icon': iconName,
};
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
}
@@ -1,313 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementView extends GetView<KRDeviceManagementController> {
const KRDeviceManagementView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15),
Color.fromRGBO(23, 151, 255, 0.05),
],
stops: [0.0, 0.28],
),
),
child: Column(
children: [
// 顶部导航栏
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Theme.of(context).iconTheme.color,
size: 20.w,
),
onPressed: () => Get.back(),
),
title: Text(
'设备管理',
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
// 内容区域
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(),
);
}
if (controller.devices.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.devices_other,
size: 64.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
SizedBox(height: 16.h),
Text(
'暂无登录设备',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () => controller.loadDeviceList(),
child: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: controller.devices.length,
itemBuilder: (context, index) {
return _buildDeviceItem(
context,
controller.devices[index],
);
},
),
);
}),
),
],
),
),
);
}
/// 构建设备项
Widget _buildDeviceItem(
BuildContext context, Map<String, dynamic> device) {
final id = device['id'] ?? '';
final identifier = device['identifier'] ?? '';
final userAgent = device['device_name'] ?? '未知设备';
final isCurrent = device['is_current'] ?? false;
final ip = device['ip'] ?? '';
final lastLoginRaw = device['last_login'];
final String lastLogin = lastLoginRaw?.toString() ?? '';
// 获取设备类型信息
final deviceInfo = controller.getDeviceTypeInfo(userAgent);
final deviceType = deviceInfo['type'] as String;
final iconName = deviceInfo['icon'] as String;
return Container(
margin: EdgeInsets.only(bottom: 12.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10.w,
offset: Offset(0, 2.w),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 设备类型和操作按钮
Row(
children: [
// 设备图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: const Color(0xFF1797FF).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.w),
),
child: Icon(
_getIconData(iconName),
color: const Color(0xFF1797FF),
size: 24.w,
),
),
SizedBox(width: 12.w),
// 设备信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
deviceType,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
if (isCurrent) ...[
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.h,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
'本机',
style: KrAppTextStyle(
fontSize: 10,
color: Colors.green,
),
),
),
],
],
),
SizedBox(height: 4.h),
Text(
'ID: ${identifier.substring(0, identifier.length > 12 ? 12 : identifier.length)}...',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
// 删除按钮
TextButton(
onPressed: () => controller.deleteDevice(id),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
),
child: Text(
'删除',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
// 分隔线
if (ip.isNotEmpty || lastLogin.isNotEmpty) ...[
SizedBox(height: 12.h),
Divider(height: 1, color: Theme.of(context).dividerColor),
SizedBox(height: 12.h),
],
// 详细信息
if (ip.isNotEmpty)
_buildInfoRow(
context,
'IP地址',
ip,
),
if (ip.isNotEmpty && lastLogin.isNotEmpty) SizedBox(height: 8.h),
if (lastLogin.isNotEmpty)
_buildInfoRow(
context,
'最后登录',
_formatDateTime(lastLoginRaw),
),
],
),
);
}
/// 构建信息行
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Row(
children: [
Text(
'$label: ',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
Expanded(
child: Text(
value,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
/// 格式化时间
String _formatDateTime(dynamic timestamp) {
if (timestamp == null) return '未知';
try {
DateTime dateTime;
if (timestamp is int) {
// 判断是秒级时间戳(10位)还是毫秒级时间戳(13位)
if (timestamp > 9999999999) {
// 毫秒级时间戳
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
} else {
// 秒级时间戳
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}
} else if (timestamp is String) {
dateTime = DateTime.parse(timestamp);
} else {
return '未知';
}
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} catch (e) {
return '未知';
}
}
/// 获取图标数据
IconData _getIconData(String iconName) {
switch (iconName) {
case 'phone_android':
return Icons.phone_android;
case 'phone_iphone':
return Icons.phone_iphone;
case 'tablet':
return Icons.tablet_mac;
case 'desktop_mac':
return Icons.desktop_mac;
case 'computer':
return Icons.computer;
default:
return Icons.devices;
}
}
}
@@ -1,253 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../localization/app_translations.dart';
import '../../../widgets/kr_app_text_style.dart';
import '../../../widgets/kr_loading_animation.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import 'kr_home_connection_info_view.dart';
import 'kr_home_connection_options_view.dart';
import 'kr_home_node_list_view.dart';
import '../widgets/kr_subscription_card.dart';
import 'kr_home_trial_card.dart';
import 'kr_home_last_day_card.dart';
import '../../../utils/kr_log_util.dart';
class KRHomeBottomPanel extends GetView<KRHomeController> {
const KRHomeBottomPanel({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
final currentStatus = controller.kr_currentListStatus.value;
KRLogUtil.kr_i('构建底部面板', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前视图状态: ${controller.kr_currentViewStatus.value}',
tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
if (controller.kr_currentListStatus.value ==
KRHomeViewsListStatus.kr_loading) {
return _kr_buildLoadingView();
}
if (controller.kr_currentListStatus.value ==
KRHomeViewsListStatus.kr_error) {
return _kr_buildErrorView(context);
}
if (currentStatus == KRHomeViewsListStatus.kr_serverList ||
currentStatus == KRHomeViewsListStatus.kr_countrySubscribeList ||
currentStatus == KRHomeViewsListStatus.kr_serverSubscribeList ||
currentStatus == KRHomeViewsListStatus.kr_subscribeList) {
return const KRHomeNodeListView();
}
return _kr_buildDefaultView(context);
});
}
Widget _kr_buildDefaultView(BuildContext context) {
// 🔧 Android 15 增强:增加防御性检查,避免空指针
bool hasValidSubscription = false;
bool isTrial = false;
bool isLastDay = false;
try {
hasValidSubscription = controller.kr_subscribeService.kr_currentSubscribe.value != null;
isTrial = controller.kr_subscribeService.kr_isTrial.value;
isLastDay = controller.kr_subscribeService.kr_isLastDayOfSubscription.value;
} catch (e) {
KRLogUtil.kr_e('获取订阅数据异常: $e', tag: 'HomeBottomPanel');
}
final isNotLoggedIn = controller.kr_currentViewStatus.value ==
KRHomeViewsStatus.kr_notLoggedIn;
KRLogUtil.kr_i('=' * 60, tag: 'HomeBottomPanel');
KRLogUtil.kr_i('🎨 构建默认视图', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否未登录: $isNotLoggedIn', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否有有效订阅: $hasValidSubscription', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('订阅列表数量: ${controller.kr_subscribeService.kr_availableSubscribes.length}', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前选中订阅: ${controller.kr_subscribeService.kr_currentSubscribe.value?.name ?? "null"}', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否试用: $isTrial', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}', tag: 'HomeBottomPanel');
// 🔧 新增:详细的 UI 渲染决策日志
if (hasValidSubscription) {
KRLogUtil.kr_i('✅ 将渲染: 连接信息卡片 (KRHomeConnectionInfoView)', tag: 'HomeBottomPanel');
} else {
KRLogUtil.kr_i('✅ 将渲染: 订阅卡片 (KRSubscriptionCard) - 开通会员界面', tag: 'HomeBottomPanel');
}
KRLogUtil.kr_i('=' * 60, tag: 'HomeBottomPanel');
// 🔧 关键修复:统一布局逻辑,确保无论登录状态如何都显示完整UI
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 主要内容区域 - 始终使用 Expanded + ScrollView 确保内容可见
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 🔧 核心修复:无论登录状态,都显示核心卡片(订阅或连接信息)
if (hasValidSubscription)
// 已订阅:显示连接信息卡片
Builder(builder: (context) {
KRLogUtil.kr_i('🔹 渲染连接信息卡片,margin top: ${12}', tag: 'HomeBottomPanel');
return Container(
margin: EdgeInsets.only(top: 12),
child: const KRHomeConnectionInfoView(),
);
})
else
// 未订阅(包括未登录):始终显示订阅卡片
Builder(builder: (context) {
KRLogUtil.kr_i('🔹 渲染订阅卡片,margin: top=${12}, left=${12}, right=${12}', tag: 'HomeBottomPanel');
return Container(
margin: EdgeInsets.only(top: 12, left: 12, right: 12),
child: const KRSubscriptionCard(),
);
}),
// 2. 如果已订阅且是试用,展示试用卡片
if (hasValidSubscription && isTrial)
Container(
margin: EdgeInsets.only(top: 12),
child: const KRHomeTrialCard(),
),
// 3. 如果已订阅且是最后一天,展示最后一天卡片
if (hasValidSubscription && isLastDay && !isTrial)
Container(
margin: EdgeInsets.only(top: 12),
child: const KRHomeLastDayCard(),
),
// 4. 连接选项(分组和国家入口)- 始终显示
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: KRHomeConnectionOptionsView(),
),
],
),
),
),
],
);
}
Widget _kr_buildLoadingView() {
KRLogUtil.kr_i('构建加载视图', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
// 🔧 Android 15 紧急修复:加载时显示完整的默认内容 + 加载指示器
// 而不是只显示一个转圈圈,避免用户看到空白面板
return Stack(
children: [
// 底层:显示默认内容(半透明)
Opacity(
opacity: 0.5,
child: _kr_buildDefaultView(Get.context!),
),
// 顶层:加载指示器
Center(
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Get.context!.theme.cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: Colors.green,
strokeWidth: 3.0,
),
SizedBox(height: 12),
Text(
'正在加载...',
style: TextStyle(
fontSize: 14,
color: Get.context!.theme.textTheme.bodyMedium?.color,
),
),
],
),
),
),
],
);
}
Widget _kr_buildErrorView(BuildContext context) {
return Container(
height: 200,
padding: EdgeInsets.all(16),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Theme.of(context).colorScheme.error,
),
SizedBox(height: 16),
Text(
AppTranslations.kr_home.error,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 8),
Text(
AppTranslations.kr_home.checkNetwork,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 24),
SizedBox(
width: 200,
height: 44,
child: ElevatedButton(
onPressed: () => controller.kr_refreshAll(),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
AppTranslations.kr_home.retry,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
);
}
}
@@ -1,260 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import '../../../widgets/kr_simple_loading.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import 'package:flutter/foundation.dart';
class KRHomeConnectionInfoView extends GetView<KRHomeController> {
const KRHomeConnectionInfoView({super.key});
@override
Widget build(BuildContext context) {
return _buildConnectCard(context);
}
/// 当前连接
Widget _buildConnectCard(BuildContext context) {
return Obx(() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16),
width: double.infinity,
height: 116,
decoration: ShapeDecoration(
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Padding(
padding: EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.currentConnectionTitle,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
// 切换节点按钮
GestureDetector(
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_subscribeList);
},
child: Row(
children: [
Text(
AppTranslations.kr_home.switchNode,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
],
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
// 🔧 修复:使用 Obx 包裹确保国旗响应式更新
Obx(() {
final countryCode = controller.kr_getCurrentNodeCountry();
if (kDebugMode) {
print('🌍 ConnectionInfo 更新,国家代码: $countryCode');
}
return KRCountryFlag(
countryCode: countryCode,
);
}),
SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.kr_currentNodeName.value,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 6),
Row(
children: [
Obx(() {
final delay = controller.kr_currentNodeLatency.value;
if (kDebugMode) {
print('🔵 UI延迟显示更新: delay=$delay');
}
// 获取延迟颜色
Color getLatencyColor(int delay) {
if (delay == -2) {
return Colors.green;
} else if (delay == -1) {
return Theme.of(context).primaryColor;
} else if (delay < 500) {
return Colors.green;
} else if (delay < 3000) {
return Color(0xFFFFB700);
} else {
return Colors.red;
}
}
// 获取延迟文本
String getLatencyText(int delay) {
if (delay == -2) {
return '--';
} else if (delay == -1) {
return AppTranslations.kr_home.connecting;
} else if (delay == 0) {
return AppTranslations.kr_home.connected;
} else if (delay >= 3000) {
return AppTranslations.kr_home.timeout;
} else {
return '${delay}ms';
}
}
// 🔧 修复:只有 delay == -1 时才显示 connecting 动画
if (delay == -1) {
return Row(
children: [
KRSimpleLoading(
color: Colors.green,
size: 12,
duration: const Duration(milliseconds: 800),
),
SizedBox(width: 2),
Text(
AppTranslations.kr_home.connecting,
style: TextStyle(
color: Colors.green,
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
);
}
return Row(
children: [
Icon(Icons.signal_cellular_alt,
size: 12,
color: getLatencyColor(delay)),
SizedBox(width: 2),
Text(
getLatencyText(delay),
style: KrAppTextStyle(
color: getLatencyColor(delay),
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
);
}),
// 只在非连接中状态显示上下行
Obx(() {
final delay = controller.kr_currentNodeLatency.value;
if (delay == -1) {
return const SizedBox.shrink();
}
return Row(
children: [
SizedBox(width: 10),
Icon(Icons.arrow_upward,
size: 12,
color: Theme.of(context).textTheme.bodySmall?.color),
Text(
controller.kr_formatBytes(KRSingBoxImp.instance.kr_stats.value.uplink),
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
SizedBox(width: 10),
Icon(Icons.arrow_downward,
size: 12,
color: Theme.of(context).textTheme.bodySmall?.color),
Text(
controller.kr_formatBytes(KRSingBoxImp.instance.kr_stats.value.downlink),
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
);
}),
],
),
],
),
],
),
// 🔧 修复: 使用多层监听确保状态更新
Obx(() {
// 🔧 关键: 强制读取两个 observable 确保追踪
final _ = KRSingBoxImp.instance.kr_status.value; // 强制追踪
final isConnected = controller.kr_isConnected.value; // 使用 controller 的状态
// 再次读取状态用于判断
final status = KRSingBoxImp.instance.kr_status.value;
final isSwitching = status is SingboxStarting || status is SingboxStopping;
// 🔧 调试日志
if (kDebugMode) {
print('🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
}
return CupertinoSwitch(
value: isConnected,
// 🔧 关键: 切换中时 onChanged 为 nullSwitch 自动禁用
onChanged: isSwitching
? null
: (bool value) {
if (kDebugMode) {
print('🔵 Switch onChanged 触发: 请求=$value, 当前状态=$status');
}
controller.kr_toggleSwitch(value);
},
activeColor: Colors.blue,
);
}),
],
),
],
),
),
);
});
}
}
@@ -1,108 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:kaer_with_panels/app/utils/kr_subscribe_navigation_util.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
class KRHomeConnectionOptionsView extends GetView<KRHomeController> {
const KRHomeConnectionOptionsView({super.key});
@override
Widget build(BuildContext context) {
print('🔌 [ConnectionOptions] 开始构建连接选项组件');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppTranslations.kr_home.connectionSectionTitle,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black87,
),
),
const SizedBox(height: 12),
_buildConnectionOption(
"home_ct",
AppTranslations.kr_home.countryRegion,
context,
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList);
},
),
],
);
}
Widget _buildConnectionOption(String icon, String label, BuildContext context,
{VoidCallback? onTap}) {
print('🔌 [ConnectionOptions] 构建连接选项: $label');
return GestureDetector(
onTap: () {
print('🔌 [ConnectionOptions] 选项被点击: $label');
if (controller.kr_subscribeService.kr_currentSubscribe.value == null) {
// 未订阅状态下,使用统一的订阅导航工具
KRSubscribeNavigationUtil.navigateToPurchase(tag: 'ConnectionOptions');
} else {
// 已订阅状态下执行原有的点击事件
onTap?.call();
}
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
KrLocalImage(
imageName: icon,
width: 36,
height: 36,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white70
: Colors.black87,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white54
: Colors.black45,
),
],
),
],
),
),
);
}
}
@@ -1,150 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../routes/app_pages.dart';
import '../../../utils/kr_subscribe_navigation_util.dart';
import '../controllers/kr_home_controller.dart';
/// 最后一天卡片组件
class KRHomeLastDayCard extends GetView<KRHomeController> {
const KRHomeLastDayCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16),
width: double.infinity,
decoration: ShapeDecoration(
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Padding(
padding: EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 顶部标题和订阅按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.lastDaySubscriptionStatus,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
GestureDetector(
onTap: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'LastDayCard'),
child: Row(
children: [
Text(
AppTranslations.kr_home.subscribe,
style: KrAppTextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.blue,
),
],
),
),
],
),
// 倒计时显示
SizedBox(height: 10),
Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer_outlined,
color: Colors.blue,
size: 16,
),
SizedBox(width: 4),
Text(
AppTranslations.kr_home.lastDaySubscriptionMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() {
final isLastDay =
controller.kr_subscribeService.kr_isLastDayOfSubscription.value;
final remainingTime = controller.kr_subscribeService.kr_subscriptionRemainingTime.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
remainingTime,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isLastDay
? (DateTime.now().millisecondsSinceEpoch %
2000 <
1000
? Colors.red
: Colors.blue)
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
SizedBox(height: 4.h),
Text(
AppTranslations.kr_home.subscriptionEndMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
),
],
);
}),
],
),
),
],
),
],
),
),
);
}
}
@@ -1,950 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import '../../../model/business/kr_outbound_item.dart';
import '../../../utils/kr_log_util.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/widgets/kr_network_image.dart';
import '../../../widgets/kr_simple_loading.dart';
import '../../../../singbox/model/singbox_proxy_type.dart';
import 'package:flutter/foundation.dart';
/// 节点列表视图组件
/// 用于展示所有节点相关的列表视图
class KRHomeNodeListView extends GetView<KRHomeController> {
const KRHomeNodeListView({super.key});
// 添加常量定义
static const Color krModernGreen = Color(0xFF4CAF50);
static const Color krModernGreenLight = Color(0xFF81C784);
// 🔧 修复无限刷新:添加标志位确保自动测试只触发一次
static bool _hasTriggeredAutoTest = false;
/// 获取显示的延迟值
/// ✅ 修复:始终显示真实的 TCP 测试结果
int _getDisplayDelay(KRHomeController controller, KROutboundItem item) {
// 直接返回真实的延迟测试结果
// 无论是否连接VPN,都使用 item.urlTestDelay.value
// - 已连接:通过 SingBox 代理测试的真实延迟
// - 未连接:通过 TCP Socket 直连测试的真实延迟
return item.urlTestDelay.value;
}
@override
Widget build(BuildContext context) {
return Obx(() {
// 根据列表状态选择不同的视图
switch (controller.kr_currentListStatus.value) {
case KRHomeViewsListStatus.kr_serverList:
return _buildServerList(context);
case KRHomeViewsListStatus.kr_subscribeList:
return _buildSubscribeList(context);
case KRHomeViewsListStatus.kr_countrySubscribeList:
return _kr_buildRegionList(context);
case KRHomeViewsListStatus.kr_serverSubscribeList:
return _kr_buildServerSubscribeList(context);
default:
return const SizedBox.shrink();
}
});
}
/// 服务器列表视图
/// 构建专用服务器列表
Widget _buildServerList(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
height: 360, // 减小高度比例
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
children: [
// 标题栏
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.serverListTitle,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
GestureDetector(
onTap: () {
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Icon(
Icons.close,
size: 24,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
// 列表内容
Expanded(
child: Obx(() {
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noServers,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: controller.kr_subscribeService.groupOutboundList.length,
itemBuilder: (context, index) {
final group =
controller.kr_subscribeService.groupOutboundList[index];
return Container(
margin: EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.1),
width: 1,
),
),
child: InkWell(
onTap: () {
controller.kr_setCurrentGroup(group);
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_serverSubscribeList;
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
KRNetworkImage(
kr_imageUrl: group.icon,
kr_width: 32,
kr_height: 32,
kr_fit: BoxFit.cover,
),
SizedBox(width: 12),
Expanded(
child: Text(
group.tag,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
),
);
},
);
}),
),
],
),
);
}
/// 国家订阅列表视图
Widget _kr_buildRegionList(BuildContext context) {
return _kr_buildListPage(
context,
title: AppTranslations.kr_home.countryListTitle,
listContent: Obx(() {
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noRegions,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return _kr_buildListContainer(
context,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(16, 8, 16, 0),
itemCount:
controller.kr_subscribeService.countryOutboundList.length,
itemBuilder: (context, index) {
final country =
controller.kr_subscribeService.countryOutboundList[index];
return Column(
children: [
// 主区域
InkWell(
onTap: () {
country.isExpand.value = !country.isExpand.value;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
KRCountryFlag(
countryCode: country.country,
width: 40,
height: 40,
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller
.kr_getCountryFullName(country.country),
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
],
),
),
Obx(() {
return Icon(
country.isExpand.value
? Icons.keyboard_arrow_down
: Icons.arrow_forward_ios,
size: 16,
color:
Theme.of(context).textTheme.bodySmall?.color,
);
}),
],
),
),
),
// 展开的服务器列表
Obx(() {
final isExpanded = country.isExpand.value;
if (!isExpanded) return const SizedBox();
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24),
itemCount: country.outboundList.length,
itemBuilder: (context, index) {
final server = country.outboundList[index];
return Column(
children: [
InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
try {
if (kDebugMode) {
print('🔄 用户点击节点: ${server.tag}');
}
// 使用统一的节点切换方法,等待完成
final success = await controller
.kr_performNodeSwitch(server.tag);
// 只有切换成功才关闭列表
if (success) {
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
if (kDebugMode) {
print('✅ 节点切换成功,关闭列表');
}
} else {
if (kDebugMode) {
print('❌ 节点切换失败,列表保持打开');
}
}
} catch (e) {
if (kDebugMode) {
print('❌ 节点切换异常: $e');
}
KRLogUtil.kr_e('节点切换异常: $e',
tag: 'NodeListView');
}
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
decoration: BoxDecoration(
// 添加轻微的背景色以区分点击区域
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8),
),
child: _kr_buildNodeListItem(
context,
item: server,
),
),
),
// 添加分隔线
if (index < country.outboundList.length - 1)
Divider(
height: 1,
indent: 16,
endIndent: 16,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
],
);
},
);
}),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
],
);
},
),
);
}),
);
}
/// 服务器订阅列表视图
// 修改服务器订阅列表视图
Widget _kr_buildServerSubscribeList(BuildContext context) {
return _kr_buildListPage(
context,
title: controller.kr_currentGroup.value?.tag ?? '',
onBack: () => controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_serverList,
listContent: Obx(() {
final servers = controller.kr_currentGroup.value?.outboundList ?? [];
if (servers.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noNodes,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return _kr_buildListContainer(
context,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
itemCount: servers.length,
itemBuilder: (context, index) {
final server = servers[index];
return Column(
children: [
InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
try {
KRLogUtil.kr_i('🔄 用户点击节点: ${server.tag}');
// 使用统一的节点切换方法,等待完成
final success = await controller
.kr_performNodeSwitch(server.tag);
// 只有切换成功才关闭列表
if (success) {
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
KRLogUtil.kr_i('✅ 节点切换成功,关闭列表');
} else {
KRLogUtil.kr_w('❌ 节点切换失败,列表保持打开');
}
} catch (e) {
KRLogUtil.kr_e('❌ 节点切换异常: $e',
tag: 'NodeListView');
}
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 4),
child: _kr_buildNodeListItem(
context,
item: server,
),
),
),
if (index < servers.length - 1)
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
],
);
},
),
);
}),
);
}
Widget _kr_buildListPage(
BuildContext context, {
required String title,
VoidCallback? onBack,
required Widget listContent,
}) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_kr_buildTitleBar(
context,
title: title,
onBack: onBack,
onClose: () =>
controller.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none,
),
SizedBox(height: 16),
Expanded(
child: Column(
children: [
Expanded(child: listContent),
// 添加底部间距
SizedBox(height: 12),
],
),
),
],
),
);
}
// 抽取公共的标题栏组件
Widget _kr_buildTitleBar(
BuildContext context, {
required String title,
VoidCallback? onBack,
VoidCallback? onClose,
}) {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
if (onBack != null) ...[
GestureDetector(
onTap: onBack,
child: Icon(
Icons.arrow_back_ios,
size: 20,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(width: 8),
],
Text(
title,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
if (onClose != null)
GestureDetector(
onTap: onClose,
child: Icon(
Icons.close,
size: 24,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
/// 构建列表容器
Widget _kr_buildListContainer(
BuildContext context, {
required Widget child,
}) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
),
child: child,
);
}
/// 构建节点列表项
Widget _kr_buildNodeListItem(
BuildContext context, {
required KROutboundItem item,
}) {
// 获取延迟颜色
Color getLatencyColor(int delay) {
if (delay == 0) {
return Colors.transparent;
} else if (delay < 500) {
return krModernGreen;
} else if (delay < 3000) {
return Color(0xFFFFB700); // 使用更容易看清的黄色
} else {
return Colors.red;
}
}
return Container(
key: ValueKey(item.id),
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 🔧 修改:显示国旗代替图标
KRCountryFlag(
countryCode: item.country,
width: 36,
height: 36,
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
item.tag,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
Obx(
() => controller.kr_cutTag.value == item.tag
? Container(
margin: EdgeInsets.only(left: 4),
padding: EdgeInsets.symmetric(
horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: krModernGreenLight.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
AppTranslations.kr_home.selected,
style: KrAppTextStyle(
fontSize: 10,
color: krModernGreen,
fontWeight: FontWeight.w500,
),
),
)
: const SizedBox.shrink(),
),
],
),
SizedBox(height: 2),
Text(
item.city,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
// 显示延迟速度
GetBuilder<KRHomeController>(
id: item.tag,
builder: (controller) {
// 获取显示的延迟值
int displayDelay = _getDisplayDelay(controller, item);
return Container(
alignment: Alignment.center,
child: Text(
displayDelay == 0
? ''
: displayDelay >= 3000
? AppTranslations.kr_home.timeout
: '${displayDelay}ms',
style: KrAppTextStyle(
fontSize: 12,
color: getLatencyColor(displayDelay),
),
),
);
},
),
],
),
);
}
// 修改订阅列表视图
Widget _buildSubscribeList(BuildContext context) {
return _kr_buildListPage(
context,
title: AppTranslations.kr_home.nodeListTitle,
listContent: Obx(() {
if (controller.kr_subscribeService.allList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noNodes,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
// 🔧 修复无限刷新:自动触发延迟测试(仅在未连接状态下,且只触发一次)
if (!_hasTriggeredAutoTest) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!controller.kr_isConnected.value && !controller.kr_isLatency.value && !_hasTriggeredAutoTest) {
_hasTriggeredAutoTest = true; // 标记已触发
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试(首次)', tag: 'NodeListView');
controller.kr_urlTest();
}
});
}
return _kr_buildListContainer(
context,
child: ListView(
padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
children: [
// 延迟测试按钮作为第一个列表项
InkWell(
onTap: () => controller.kr_urlTest(),
child: Container(
padding: EdgeInsets.symmetric(vertical: 8),
margin: EdgeInsets.only(top: 8), // 添加上方间距
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: krModernGreenLight.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: controller.kr_isLatency.value
? KRSimpleLoading(
color: krModernGreen,
size: 24,
duration: const Duration(milliseconds: 800),
)
: Icon(
Icons.speed,
size: 24,
color: krModernGreen,
),
),
),
SizedBox(width: 8),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.kr_isLatency.value
? AppTranslations.kr_home.testing
: AppTranslations.kr_home.testLatency,
style: KrAppTextStyle(
fontSize: 14,
color: controller.kr_isLatency.value
? Theme.of(context)
.textTheme
.bodySmall
?.color
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
fontWeight: controller.kr_isLatency.value
? FontWeight.normal
: FontWeight.w500,
),
),
if (!controller.kr_isLatency.value) ...[
SizedBox(height: 2),
Text(
AppTranslations.kr_home.refreshLatencyDesc,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
),
],
],
),
),
if (!controller.kr_isLatency.value)
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
),
// 分隔线
Divider(
height: 16,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// Auto 选项
InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
try {
final success =
await controller.kr_performNodeSwitch('auto');
if (success) {
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
}
} catch (e) {
KRLogUtil.kr_e('Auto选项切换异常: $e',
tag: 'NodeListView');
}
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: "home_list_location",
width: 36,
height: 36,
color: controller.kr_cutTag.value == 'auto'
? Colors.green
: null,
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
AppTranslations.kr_home.autoSelect,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
if (controller.kr_cutTag.value == 'auto')
Container(
margin: EdgeInsets.only(left: 4),
padding: EdgeInsets.symmetric(
horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color:
krModernGreenLight.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
AppTranslations.kr_home.selected,
style: KrAppTextStyle(
fontSize: 10,
color: krModernGreen,
fontWeight: FontWeight.w500,
),
),
),
],
),
SizedBox(height: 2),
Obx(() {
// 获取当前自动选择的节点
String selectedNode =
AppTranslations.kr_home.autoSelect;
int delay = 0;
for (var group
in KRSingBoxImp.instance.kr_activeGroups) {
if (group.type == ProxyType.urltest) {
selectedNode = group.selected;
delay = controller
.kr_subscribeService.keyList[group.selected]
?.urlTestDelay.value ??
0;
break;
}
}
return Text(
selectedNode,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
);
}),
],
),
),
Obx(() {
// 获取当前自动选择的节点
String selectedNode =
AppTranslations.kr_home.autoSelect;
int delay = 0;
for (var group
in KRSingBoxImp.instance.kr_activeGroups) {
if (group.type == ProxyType.urltest) {
selectedNode = group.selected;
delay = controller
.kr_subscribeService
.keyList[group.selected]
?.urlTestDelay
.value ??
0;
break;
}
}
return delay > 0
? Container(
alignment: Alignment.center,
child: Text(
delay < 3000
? '${delay}ms'
: AppTranslations.kr_home.timeout,
style: KrAppTextStyle(
fontSize: 12,
color: delay < 3000
? Colors.green
: Colors.red,
),
),
)
: const SizedBox.shrink();
}),
],
),
),
),
// 分隔线
Divider(
height: 16,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// 节点列表
...controller.kr_subscribeService.allList
.map((node) => Column(
children: [
InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
try {
KRLogUtil.kr_i(
'🔄 用户点击节点: ${node.tag}');
final success = await controller
.kr_performNodeSwitch(node.tag);
if (success) {
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
}
} catch (e) {
KRLogUtil.kr_e(
'节点切换异常: $e',
tag: 'NodeListView');
}
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 4),
child: _kr_buildNodeListItem(
context,
item: node,
),
),
),
if (node !=
controller.kr_subscribeService.allList.last)
Divider(
height: 1,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
],
))
.toList(),
],
),
);
}),
);
}
}
@@ -1,223 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:math';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
class KRHomeSubscriptionView extends GetView<KRHomeController> {
const KRHomeSubscriptionView({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
if (!KRAppRunData().kr_isLogin.value) {
return const SizedBox.shrink();
}
final currentSubscribe =
controller.kr_subscribeService.kr_currentSubscribe.value;
if (currentSubscribe == null) {
return const SizedBox.shrink();
}
KRLogUtil.kr_i('当前订阅名称: ${currentSubscribe.name}',
tag: 'SubscriptionView');
final totalTraffic = currentSubscribe.traffic;
final usedTraffic = currentSubscribe.download + currentSubscribe.upload;
final hasTrafficLimit = totalTraffic > 0;
var trafficPercentage =
hasTrafficLimit ? (usedTraffic / totalTraffic).clamp(0.0, 1.0) : 0.0;
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bolt_rounded,
size: 14.w,
color: Theme.of(context).brightness == Brightness.light
? Colors.black54
: Colors.white54,
),
SizedBox(width: 4.w),
Expanded(
child: Text(
currentSubscribe.name,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 4.w),
Container(
height: 3.h,
width: 20.w,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[200]
: Colors.grey[800],
borderRadius: BorderRadius.circular(1.5.r),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: trafficPercentage,
child: Container(
decoration: BoxDecoration(
color: _getTrafficColor(trafficPercentage),
borderRadius: BorderRadius.circular(1.5.r),
),
),
),
),
SizedBox(width: 4.w),
Icon(
Icons.swap_horiz,
size: 14.w,
color: Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(0.5)
: Colors.white.withOpacity(0.5),
),
],
),
);
});
}
Widget _buildLoadingView(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.grey[900]!,
Theme.of(context).brightness == Brightness.light
? Colors.grey[50]!
: Colors.grey[800]!,
],
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
const SizedBox(width: 16),
Text(
AppTranslations.kr_home.loading,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
IconData _getStatusIcon(KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return Icons.error_outline_rounded;
} else if (difference.inDays <= 1) {
return Icons.warning_amber_rounded;
} else {
return Icons.check_circle_outline_rounded;
}
}
Color _getStatusColor(
BuildContext context, KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return Colors.red;
} else if (difference.inDays <= 1) {
return Colors.orange;
} else {
return const Color(0xFF00E52B);
}
}
String _getStatusText(KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return '已过期';
} else if (difference.inDays <= 1) {
return '即将到期';
} else {
return '有效';
}
}
Color _getTrafficColor(double percentage) {
if (percentage >= 0.9) {
return Colors.red;
} else if (percentage >= 0.7) {
return Colors.orange;
} else {
return const Color(0xFF00E52B);
}
}
String _formatTraffic(int bytes) {
if (bytes < 1024) {
return '$bytes B';
} else if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
} else if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
} catch (e) {
return dateStr;
}
}
}
@@ -1,149 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../routes/app_pages.dart';
import '../../../services/kr_subscribe_service.dart';
import '../../../utils/kr_log_util.dart';
import '../../../utils/kr_subscribe_navigation_util.dart';
import '../controllers/kr_home_controller.dart';
/// 试用卡片组件
class KRHomeTrialCard extends GetView<KRHomeController> {
const KRHomeTrialCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16),
width: double.infinity,
decoration: ShapeDecoration(
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Padding(
padding: EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 顶部标题和订阅按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.trialStatus,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
GestureDetector(
onTap: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'TrialCard'),
child: Row(
children: [
Text(
AppTranslations.kr_home.subscribe,
style: KrAppTextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.blue,
),
],
),
),
],
),
// 倒计时显示
SizedBox(height: 10),
Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer_outlined,
color: Colors.blue,
size: 16,
),
SizedBox(width: 4),
Text(
AppTranslations.kr_home.trialing,
style: KrAppTextStyle(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCountdown(),
],
),
),
],
),
],
),
),
);
}
Widget _buildCountdown() {
return Obx(() {
final subscribeService = KRSubscribeService();
final remainingTime = subscribeService.kr_trialRemainingTime.value;
final isExpired = remainingTime.isEmpty;
return Builder(
builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
remainingTime,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isExpired
? (DateTime.now().millisecondsSinceEpoch % 2000 < 1000
? Colors.red
: Colors.blue)
: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 4.h),
Text(
AppTranslations.kr_home.trialEndMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
});
}
}
+10 -16
View File
@@ -11,9 +11,6 @@ import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
import '../../../services/kr_subscribe_service.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import '../widgets/kr_subscribe_selector_view.dart';
import 'kr_home_bottom_panel.dart';
import 'kr_home_subscription_view.dart';
import './hi_animated_connect_button.dart';
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
@@ -138,18 +135,15 @@ class _KRHomeViewState extends State<KRHomeView> {
style: highlightStyle,
);
} else {
// --- 情况2.2: 订阅有效 ---
final difference =
expireDateTime?.difference(now);
final remainingDaysText =
(difference?.inDays ?? 0) > 0
? '${difference!.inDays}'
: '不足一天';
// 使用换行符 \n 合并为单个 Text 组件
content = Text(
'套餐剩余:$remainingDaysText\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
style: normalStyle,
);
// --- 情况2.2: 订阅有效 ---
final formattedExpireTime = expireDateTime != null
? '${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')} ${expireDateTime.hour.toString().padLeft(2, '0')}:${expireDateTime.minute.toString().padLeft(2, '0')}'
: '未知';
// 使用换行符 \n 合并为单个 Text 组件
content = Text(
'套餐到期时间:$formattedExpireTime\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
style: normalStyle,
);
}
}
@@ -221,7 +215,7 @@ class _KRHomeViewState extends State<KRHomeView> {
GestureDetector(
onTap: () {
HIDialog.show(
title: '*闪连功能',
title: '闪连功能',
message:
'开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
);
@@ -1,275 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
class KRSubscribeSelectorView extends StatelessWidget {
final KRHomeController? controller;
const KRSubscribeSelectorView({
super.key,
this.controller,
});
@override
Widget build(BuildContext context) {
final homeController = controller ?? Get.find<KRHomeController>();
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container(
width: MediaQuery.of(context).size.width * 0.85,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10.r,
offset: Offset(0, 2.w),
spreadRadius: 0,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.05),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.r),
topRight: Radius.circular(20.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_purchaseMembership.selectPackage,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.titleLarge?.color,
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(20.r),
child: Padding(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.close_rounded,
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.6),
size: 18.w,
),
),
),
),
],
),
),
Obx(() {
final subscribes = homeController.kr_subscribeService.kr_availableSubscribes;
if (subscribes.isEmpty) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 12.w),
child: Column(
children: [
Icon(
Icons.subscriptions_outlined,
size: 48.w,
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.3),
),
SizedBox(height: 12.h),
Text(
AppTranslations.kr_purchaseMembership.noData,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.5),
),
),
],
),
);
}
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.5,
),
child: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 4.w),
itemCount: subscribes.length,
itemBuilder: (context, index) {
final subscribe = subscribes[index];
final isCurrent = subscribe.id == homeController.kr_subscribeService.kr_currentSubscribe.value?.id;
return _SubscribeItem(
subscribe: subscribe,
isCurrent: isCurrent,
onTap: () {
homeController.kr_switchSubscribe(subscribe);
Get.back();
},
);
},
),
);
}),
SizedBox(height: 8.h),
],
),
);
}
}
class _SubscribeItem extends StatelessWidget {
final KRUserAvailableSubscribeItem subscribe;
final bool isCurrent;
final VoidCallback onTap;
const _SubscribeItem({
required this.subscribe,
required this.isCurrent,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final usedTraffic = (subscribe.download + subscribe.upload) / 1024 / 1024 / 1024;
final totalTraffic = subscribe.traffic / 1024 / 1024 / 1024;
var percentage = totalTraffic > 0 ? usedTraffic / totalTraffic : 0.0;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final isUnlimited = subscribe.traffic == 0;
String getUsedTrafficDisplay() {
if (usedTraffic < 1) {
return '${(usedTraffic * 1024).toStringAsFixed(2)}MB';
} else {
return '${usedTraffic.toStringAsFixed(2)}GB';
}
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.r),
child: Ink(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isCurrent
? Colors.blue.withOpacity(isDarkMode ? 0.15 : 0.08)
: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isCurrent
? Colors.blue.withOpacity(isDarkMode ? 0.5 : 0.3)
: Theme.of(context).dividerColor.withOpacity(0.3),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
subscribe.name,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.titleLarge?.color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (isCurrent)
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
blurRadius: 6.w,
offset: Offset(0, 1.w),
),
],
),
child: Text(
AppTranslations.kr_home.currentConnectionTitle,
style: KrAppTextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 8.h),
Text(
isUnlimited
? AppTranslations.kr_purchaseMembership.unlimitedTraffic
: '${getUsedTrafficDisplay()} / ${totalTraffic.toStringAsFixed(2)}GB',
style: KrAppTextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8),
),
),
if (!isUnlimited) ...[
SizedBox(height: 8.h),
ClipRRect(
borderRadius: BorderRadius.circular(4.r),
child: LinearProgressIndicator(
value: percentage.clamp(0.0, 1.0),
backgroundColor: isDarkMode
? Colors.grey[700]?.withOpacity(0.7)
: Colors.grey[300]?.withOpacity(0.9),
valueColor: AlwaysStoppedAnimation<Color>(
_getTrafficColor(percentage, isDarkMode),
),
minHeight: 4.w,
),
),
],
],
),
),
),
),
);
}
Color _getTrafficColor(double percentage, bool isDarkMode) {
if (percentage >= 0.9) {
return isDarkMode
? Colors.red.withOpacity(0.8)
: Colors.red.withOpacity(0.7);
} else if (percentage >= 0.7) {
return isDarkMode
? Colors.orange.withOpacity(0.8)
: Colors.orange.withOpacity(0.7);
} else {
return isDarkMode
? Colors.blue.withOpacity(0.8)
: Colors.blue.withOpacity(0.7);
}
}
}
@@ -1,116 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:kaer_with_panels/app/utils/kr_subscribe_navigation_util.dart';
import '../../../widgets/kr_app_text_style.dart';
/// 订阅卡片组件
class KRSubscriptionCard extends StatelessWidget {
const KRSubscriptionCard({
super.key,
});
@override
Widget build(BuildContext context) {
return _kr_buildSubscriptionCard(context);
}
// 构建订阅卡片
Widget _kr_buildSubscriptionCard(BuildContext context) {
// 🔧 关键修复:完全移除 ScreenUtil,使用固定像素值避免缩放问题
return Container(
// 添加固定高度,确保卡片可见
constraints: const BoxConstraints(minHeight: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 图标
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.language,
color: Colors.blue,
size: 28,
),
),
const SizedBox(height: 16),
// 描述文字
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
AppTranslations.kr_home.subscriptionDescription,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
height: 1.5,
// 🔧 关键修复:确保文本颜色可见
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black87,
),
),
),
const SizedBox(height: 20),
// 订阅按钮
SizedBox(
width: double.infinity,
height: 46,
child: ElevatedButton(
onPressed: () {
KRSubscribeNavigationUtil.navigateToPurchase(tag: 'SubscriptionCard');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
AppTranslations.kr_home.subscribe,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
],
),
);
}
Widget _kr_buildListContainer(
BuildContext context, {
required Widget child,
EdgeInsetsGeometry? margin,
bool addBottomPadding = true,
}) {
return Container(
margin: margin ?? EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
),
child: IntrinsicWidth(
child: child,
),
);
}
}
@@ -43,6 +43,7 @@ class KRInviteController extends GetxController {
totalCommission: 0,
).obs;
final kr_referCode = ''.obs;
final kr_refererId = 0.obs;
final kr_isLoading = false.obs;
final count = 0.obs;
final EasyRefreshController refreshController = EasyRefreshController();
@@ -65,6 +66,7 @@ class KRInviteController extends GetxController {
totalCommission: 0,
);
kr_referCode.value = '';
kr_refererId.value = 0;
}
});
if (KRAppRunData.getInstance().kr_isLogin.value) {
@@ -87,11 +89,14 @@ class KRInviteController extends GetxController {
KRLogUtil.kr_i(' - kr_isLogin: ${appData.kr_isLogin.value}', tag: 'InviteController');
KRLogUtil.kr_i(' - kr_account: ${appData.kr_account.value}', tag: 'InviteController');
KRLogUtil.kr_i(' - kr_referCode: ${appData.kr_referCode.value}', tag: 'InviteController');
KRLogUtil.kr_i(' - kr_refererId: ${appData.kr_refererId.value}', tag: 'InviteController');
KRLogUtil.kr_i(' - kr_balance: ${appData.kr_balance.value}', tag: 'InviteController');
KRLogUtil.kr_i(' - kr_commission: ${appData.kr_commission.value}', tag: 'InviteController');
kr_referCode.value = appData.kr_referCode.value;
kr_refererId.value = appData.kr_refererId.value;
KRLogUtil.kr_i('📋 [InviteController] 获取到邀请码: "${kr_referCode.value}"', tag: 'InviteController');
KRLogUtil.kr_i('📋 [InviteController] 获取到邀请人ID: ${kr_refererId.value}', tag: 'InviteController');
if (kr_referCode.value.isEmpty) {
KRLogUtil.kr_w('⚠️ [InviteController] 邀请码为空!', tag: 'InviteController');
@@ -148,19 +153,23 @@ class KRInviteController extends GetxController {
/// 处理绑定邀请码
Future<void> kr_handleBindInviteCode() async {
final text = otherInviteCodeController.text;
print('输入的邀请码是: $text');
if (text.isEmpty) {
KRCommonUtil.kr_showToast('请输入邀请码');
return;
}
if (text.trim().toLowerCase() == kr_referCode.value.trim().toLowerCase()) {
KRCommonUtil.kr_showToast('您不可以邀请自己');
return;
}
try {
final either = await KRUserApi().hi_inviteCode(text);
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(affiliateCount) {
(success) {
KRCommonUtil.kr_showToast('绑定成功: $text');
otherInviteCodeController.text = '';
_kr_fetchUserInfo(); // 刷新用户信息以更新 refererId 并隐藏输入框
},
);
} catch (e) {
@@ -29,187 +29,201 @@ class KRInviteView extends GetView<KRInviteController> {
children: [
// 1. 背景层/滚动层
Positioned.fill(
child: SingleChildScrollView(
// 保持手动管理,增强稳定性
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.manual,
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 40.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20.w),
// 🟢 第一行:奖励说明
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(25.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: 'hi-home-logo',
imageType: ImageType.svg,
width: 54.w,
color: Colors.black,
),
SizedBox(width: 16.w),
Flexible(
child: Text(
'受邀用户首次付款时,他将与您分别获得3天免费使用时长',
style: TextStyle(
color: Colors.black,
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
SizedBox(height: 26.w),
// 🟢 第二行:我的邀请码
Container(
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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 40.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20.w),
// 🟢 第一行:奖励说明
Container(
width: 100.w,
height: 40.w,
alignment: Alignment.center,
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(25.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: 'hi-home-logo',
imageType: ImageType.svg,
width: 54.w,
color: Colors.black,
),
SizedBox(width: 16.w),
Flexible(
child: Text(
'受邀用户首次付款时,他将与您分别获得3天免费使用时长',
style: TextStyle(
color: Colors.black,
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
SizedBox(height: 26.w),
// 🟢 第二行:我的邀请码
Container(
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: Text(
'邀请码',
style: TextStyle(
color: Colors.black,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
child: Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 100.w,
height: 40.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(1000.r),
),
child: Text(
'邀请码',
style: TextStyle(
color: Colors.black,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
controller.kr_referCode.value,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const KrLocalImage(
imageName: 'share-icon',
imageType: ImageType.svg,
color: Colors.white,
),
onPressed: () {
if (controller.kr_referCode.value.isNotEmpty) {
final code = controller.kr_referCode.value;
final text = '#您的好友邀请您使用Hi快网络加速器\n'
'安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n'
'$code\n'
'您和您的好友将会分别获得3天免费时长\n\n'
'点击此处进入下载页面\n'
'或在浏览器输入hifastvpn.com下载#';
if (GetPlatform.isIOS) {
Share.share(text, subject: '直接分享Hi快VPN邀请链接');
} else {
Clipboard.setData(ClipboardData(text: text));
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.inviteCodeCopied);
}
}
},
),
],
),
),
),
Expanded(
child: Text(
controller.kr_referCode.value,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const KrLocalImage(
imageName: 'share-icon',
imageType: ImageType.svg,
color: Colors.white,
),
onPressed: () {
if (controller.kr_referCode.value.isNotEmpty) {
final code = controller.kr_referCode.value;
final text = '#您的好友邀请您使用Hi快网络加速器\n'
'安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n'
'$code\n'
'您和您的好友将会分别获得3天免费时长\n\n'
'点击此处进入下载页面\n'
'或在浏览器输入hifastvpn.com下载#';
if (GetPlatform.isIOS) {
Share.share(text, subject: '直接分享Hi快VPN邀请链接');
} else {
Clipboard.setData(ClipboardData(text: text));
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.inviteCodeCopied);
}
}
},
),
const Spacer(),
// 🟢 第三部分:接受他人邀请
Obx(() {
// 只有当没有被邀请时才显示
if (controller.kr_refererId.value != 0) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'接受他人邀请',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
RepaintBoundary(
child: TextField(
controller: controller.otherInviteCodeController,
textAlign: TextAlign.center,
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),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
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),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
),
constraints: BoxConstraints(maxHeight: 50.h),
),
),
),
SizedBox(height: 10.w),
SizedBox(
width: double.infinity,
height: 50.w,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(1000.r),
),
),
onPressed: () => controller.kr_handleBindInviteCode(),
child: Text(
'保存',
style: TextStyle(
color: Colors.black,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}),
// 底部留白,确保键盘弹出后能滚过遮挡区域
SizedBox(height: 90.w),
],
),
),
),
SizedBox(height: 160.w),
// 🟢 第三部分:接受他人邀请
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'接受他人邀请',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
// 使用 RepaintBoundary 隔离,减少父级重绘对 TextField 的影响
RepaintBoundary(
child: TextField(
controller: controller.otherInviteCodeController,
textAlign: TextAlign.center,
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),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
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),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
),
constraints: BoxConstraints(maxHeight: 50.h),
),
),
),
SizedBox(height: 10.w),
SizedBox(
width: double.infinity,
height: 50.w,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(1000.r),
),
),
onPressed: () => controller.kr_handleBindInviteCode(),
child: Text(
'保存',
style: TextStyle(
color: Colors.black,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
// 底部留白,确保键盘弹出后能滚过遮挡区域
SizedBox(height: 250.w),
],
),
),
),
);
},
),
),
@@ -1,12 +0,0 @@
import 'package:get/get.dart';
import '../controllers/kr_language_selector_controller.dart';
class KRLanguageSelectorBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRLanguageSelectorController>(
() => KRLanguageSelectorController(),
);
}
}
@@ -1,42 +0,0 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
class KRLanguageSelectorController extends GetxController {
// 使用 KRLanguage 枚举来加载语言
final RxList<KRLanguage> kr_languages = <KRLanguage>[].obs;
// 当前选中的语言代码
final RxString kr_selectedLanguage = ''.obs;
@override
void onInit() {
super.onInit();
kr_selectedLanguage.value = KRLanguageUtils.getCurrentLanguage().countryCode;
kr_loadLanguages();
}
// 加载语言数据
void kr_loadLanguages() {
// 将英语放在前面
final sortedLanguages = KRLanguage.values.toList()
..sort((a, b) => a == KRLanguage.en ? -1 : 1);
kr_languages.value = sortedLanguages;
}
// 选择语言
Future<void> kr_selectLanguage(KRLanguage language) async {
try {
// 先更新选中状态
kr_selectedLanguage.value = language.countryCode;
// 然后切换语言
await KRLanguageUtils.switchLanguage(language);
} catch (err) {
Get.snackbar(
'错误',
'切换语言失败: $err',
snackPosition: SnackPosition.BOTTOM,
);
}
}
}
@@ -1,115 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_language_selector_controller.dart';
class KRLanguageSelectorView extends GetView<KRLanguageSelectorController> {
const KRLanguageSelectorView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
// 非渐变色区域
],
stops: [0.0, 0.28], // 调整渐变结束位置
),
),
child: Column(
children: [
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_setting.switchLanguage,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => ListView.separated(
padding: EdgeInsets.all(16.r),
itemCount: controller.kr_languages.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final language = controller.kr_languages[index];
return _kr_buildLanguageCard(language, context);
},
),
),
),
],
),
),
);
}
// 构建语言卡片
Widget _kr_buildLanguageCard(KRLanguage language, BuildContext context) {
return InkWell(
onTap: () => controller.kr_selectLanguage(language),
child: Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
// 国旗图标
CircleAvatar(
radius: 16.r,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Text(
language.flagEmoji,
style: TextStyle(fontSize: 16),
),
),
SizedBox(width: 12.w),
// 语言名称
Text(
language.languageName,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
// 选中标记
if (controller.kr_selectedLanguage.value == language.countryCode)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.r,
),
],
),
),
);
}
}
@@ -80,6 +80,7 @@ class KRLoginController extends GetxController
/// 验证码倒计时
var _countdown = 60; // 倒计时初始值
DateTime? _endTime; // 倒计时结束时间
late Timer _timer;
var kr_countdownText = AppTranslations.kr_login.sendCode.obs;
@@ -294,6 +295,11 @@ class KRLoginController extends GetxController
return;
}
if (!validateEmail(accountController.text.trim())) {
KRCommonUtil.kr_showToast('请输入有效的邮箱地址');
return;
}
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
@@ -306,7 +312,12 @@ class KRLoginController extends GetxController
/// 发送验证码(仅支持邮箱)
void kr_sendCode() async {
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
KRCommonUtil.kr_showToast('请输入邮箱');
return;
}
if (!validateEmail(accountController.text.trim())) {
KRCommonUtil.kr_showToast('请输入有效的邮箱地址');
return;
}
@@ -356,8 +367,14 @@ class KRLoginController extends GetxController
/// 开始注册(仅支持邮箱,验证码和邀请码可选)
void kr_register() async {
// 验证邮箱
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
final email = accountController.text.trim();
if (email.isEmpty) {
KRCommonUtil.kr_showToast('请输入邮箱');
return;
}
if (!validateEmail(email)) {
KRCommonUtil.kr_showToast('请输入有效的邮箱地址');
return;
}
@@ -509,21 +526,31 @@ class KRLoginController extends GetxController
/// 开始倒计时
void _startCountdown() {
kr_canSendCode.value = false;
_endTime = DateTime.now().add(const Duration(seconds: 60));
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_countdown > 0) {
_countdown -= 1;
kr_countdownText.value = "${_countdown}s";
_timer.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
final now = DateTime.now();
if (_endTime != null && _endTime!.isAfter(now)) {
final remaining = _endTime!.difference(now).inSeconds;
_countdown = remaining;
kr_countdownText.value = "${remaining}s";
} else {
kr_canSendCode.value = true;
kr_countdownText.value = AppTranslations.kr_login.sendCode;
_countdown = 60;
_endTime = null;
timer.cancel();
_onCountdownFinished();
}
});
}
/// 验证邮箱格式
bool validateEmail(String str) {
return RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$').hasMatch(str);
}
/// 设置登录数据(仅支持邮箱)
void _saveLoginData(String token) {
KRAppRunData.getInstance().kr_saveUserInfo(
@@ -1,24 +0,0 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/kr_area_code.dart'; // 假设这个文件中有 KRAreaCode 类
class KRSearchAreaController extends GetxController {
final areas = <KRAreaCodeItem>[].obs;
final searchQuery = ''.obs;
@override
void onInit() {
super.onInit();
areas.assignAll(KRAreaCode.kr_getCodeList());
}
List<KRAreaCodeItem> get filteredAreas {
if (searchQuery.value.isEmpty) {
return areas;
} else {
return areas
.where((area) => area.kr_dialCode.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
}
}
@@ -117,46 +117,6 @@ class KRLoginView extends GetView<KRLoginController> {
);
}
Widget _buildContentByEntry() {
final entry = (Get.arguments as Map<String, dynamic>?)?['entry'];
if (entry == 'forget_psd') {
return _buildForgetPasswordLayout();
} else if (entry == 'bind_email') {
return _buildBindEmailLayout();
} else if (entry == 'login') {
return _buildLoginEmailLayout();
}
return _buildForgetPasswordLayout(); // 默认显示修改密码
}
Widget _buildForgetPasswordLayout() {
return Column(
children: [
Container(
constraints: BoxConstraints(minHeight: 300.w),
child: Column(
children: [
_buildStandardInputField(
controller: controller.psdController,
hintText: '新密码',
isPassword: true,
),
SizedBox(height: 10.w),
_buildStandardInputField(
controller: controller.agPsdController,
hintText: '确认密码',
isPassword: true,
),
],
),
),
SizedBox(height: 30.h),
_buildSaveButton(),
],
);
}
Widget _buildBindEmailLayout() {
return Column(
children: [
@@ -192,33 +152,6 @@ class KRLoginView extends GetView<KRLoginController> {
);
}
Widget _buildLoginEmailLayout() {
return Column(
children: [
Container(
constraints: BoxConstraints(minHeight: 300.w),
child: Column(
children: [
_buildStandardInputField(
controller: controller.accountController,
hintText: 'Email',
),
SizedBox(height: 10),
_buildStandardInputField(
controller: controller.psdController,
hintText: '密码',
isPassword: true,
),
SizedBox(height: 10.w),
],
),
),
SizedBox(height: 30.h),
_buildSaveButton(),
],
);
}
/// 构建标准输入框
Widget _buildStandardInputField({
required TextEditingController controller,
@@ -283,14 +216,6 @@ class KRLoginView extends GetView<KRLoginController> {
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);
@@ -1,131 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/kr_area_code.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_search_area_controller.dart';
class KRSearchAreaView extends GetView<KRSearchAreaController> {
final Function(KRAreaCodeItem, int) onSelect;
const KRSearchAreaView({super.key, required this.onSelect});
static void show(Function(KRAreaCodeItem, int) onSelect) {
Get.dialog(
KRSearchAreaView(onSelect: onSelect),
barrierDismissible: true,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // 获取当前主题
Get.lazyPut<KRSearchAreaController>(
() => KRSearchAreaController(),
);
return GestureDetector(
onTap: () => Get.back(), // 点击背景关闭弹框
child: Scaffold(
backgroundColor: Colors.black.withOpacity(0.0),
body: Center(
child: GestureDetector(
onTap: () {}, // 阻止点击事件传递到背景
child: Container(
width: 300.w,
height: 450.h,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: theme.primaryColor,
borderRadius: BorderRadius.circular(15.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'login.selectOtherRegion'.tr,
style: KrAppTextStyle(
fontSize: 15.w,
fontWeight: FontWeight.w500,
color: theme.textTheme.titleMedium?.color),
),
SizedBox(height: 10.h),
TextField(
onChanged: (value) => controller.searchQuery.value = value,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
hintText: 'login.search'.tr,
hintStyle: TextStyle(color: Colors.grey),
filled: true,
// fillColor: Colors.grey.shade200,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.w),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 10.h),
),
style: theme.textTheme.bodyMedium?.copyWith(fontSize: 14.sp, fontFamily: 'AlibabaPuHuiTi-Regular',),
),
// SizedBox(height: 5.h),
Obx(() => Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: controller.filteredAreas.length,
itemBuilder: (context, index) {
final area = controller.filteredAreas[index];
return GestureDetector(
onTap: () {
onSelect(area, index); // 调用回调函数
Get.back();
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 10.h, horizontal: 0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey.shade300,
width: 0.2,
),
),
),
child: Row(
children: [
Text(area.kr_icon,
style: TextStyle(fontSize: 20.w)),
SizedBox(width: 12.w),
Expanded(
child: Text(
area.kr_name,
style: KrAppTextStyle(
fontSize: 13.w,
fontWeight: FontWeight.w500),
),
),
Text(
"+" + area.kr_dialCode,
style: KrAppTextStyle(
fontSize: 13.w,
fontWeight: FontWeight.w500),
),
],
),
),
);
},
),
)),
],
),
),
),
),
),
);
}
}
@@ -246,13 +246,13 @@ class KRPurchaseMembershipController extends GetxController {
/// 获取用户已订阅套餐
Future<void> kr_getAlreadySubscribe() async {
final either = await _kr_subscribeApi.kr_getAlreadySubscribe();
final either = await _kr_subscribeApi.kr_getAlreadySubscribe(includeExpired: 'all');
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(alreadySubscribe) {
_kr_alreadySubscribe = alreadySubscribe;
KRLogUtil.kr_i(
'订阅套餐: ${_kr_alreadySubscribe.map((e) => e.subscribeId).toList()}',
'获取所有订阅记录(含过期): ${_kr_alreadySubscribe.map((e) => "ID:${e.userSubscribeId}, SubID:${e.subscribeId}").toList()}',
tag: 'PurchaseMembershipController');
},
);
@@ -558,19 +558,18 @@ class KRPurchaseMembershipController extends GetxController {
// =========================================================================
final quantity = kr_getSelectedQuantity();
// 判断是续订还是新购
final isRenewal = _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
final subscribeId = isRenewal
? _kr_alreadySubscribe
.firstWhere(
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id,
orElse: () =>
KRAlreadySubscribe(userSubscribeId: 0, subscribeId: 0), // 默认值
)
.userSubscribeId
// 判断是续订还是新购:查找匹配选中套餐 ID 的最后一项记录(最新的记录,可能已过期)
final matchingSubscribes = _kr_alreadySubscribe
.where((subscribe) => subscribe.subscribeId == selectedPlan.kr_id)
.toList();
final bool isRenewal = matchingSubscribes.isNotEmpty;
final int subscribeId = isRenewal
? matchingSubscribes.last.userSubscribeId
: 0;
print('📊 [Purchase] 订阅判断: isRenewal=$isRenewal, userSubscribeId=$subscribeId');
// 根据判断结果调用不同的接口
final purchaseEither = isRenewal
? await _kr_subscribeApi.kr_renewal(
@@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
/// 套餐详情弹框
class KRPlanDetailsDialog extends StatelessWidget {
final List<KRFeature> kr_features;
const KRPlanDetailsDialog({
Key? key,
required this.kr_features,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_purchaseMembership.planDetails,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
],
),
const SizedBox(height: 16),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: kr_features.length,
itemBuilder: (context, index) {
final feature = kr_features[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
feature.kr_label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
...feature.kr_details.map((detail) => Padding(
padding: const EdgeInsets.only(left: 16, bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.check_circle_outline,
size: 16,
color: Colors.green,
),
const SizedBox(width: 8),
Expanded(
child: Text(
detail.kr_description,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
],
),
)),
if (index < kr_features.length - 1)
const Divider(height: 24),
],
);
},
),
),
],
),
),
);
}
}
@@ -1,12 +0,0 @@
import 'package:get/get.dart';
import '../controllers/kr_setting_controller.dart';
class KRSettingBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRSettingController>(
() => KRSettingController(),
);
}
}
@@ -1,170 +0,0 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import '../../../localization/app_translations.dart';
import '../../../themes/kr_theme_service.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRSettingController extends GetxController {
// 创建 AppTranslationsSetting 的实例
final AppTranslationsSetting kr_appTranslationsSetting =
AppTranslationsSetting();
// 当前选择的国家
final RxString kr_currentCountry = ''.obs;
// 自动连接开关
final RxBool kr_autoConnect = true.obs;
// 通知开关
final RxBool kr_notification = true.obs;
// 帮助改进开关
final RxBool kr_helpImprove = true.obs;
// 版本号
final RxString kr_version = ''.obs;
// IOS评分
final String kr_iosRating = '';
// 当前语言
final RxString kr_language = ''.obs;
// 当前主题选项
final RxString kr_themeOption = ''.obs;
final RxString kr_vpnMode = ''.obs;
final RxString kr_vpnModeRemark = ''.obs;
// 修改 VPN 模式切换方法
void kr_changeVPNMode(String mode) {
KRLogUtil.kr_i('设置的VPN模式文本: ${kr_vpnMode.value}', tag: 'SettingController');
}
// 切换语言
void kr_changeLanguage() {
Get.toNamed(Routes.KR_LANGUAGE_SELECTOR);
}
// 删除账号
void kr_deleteAccount() {
// 检查是否已登录
if (!KRAppRunData.getInstance().kr_isLogin.value) {
// 如果未登录,跳转到登录页面
// Get.toNamed(Routes.MR_LOGIN);
return;
}
// 已登录,跳转到删除账号页面
Get.toNamed(Routes.KR_DELETE_ACCOUNT);
}
@override
void onInit() {
super.onInit();
_loadThemeOption();
kr_language.value = KRLanguageUtils.getCurrentLanguage().languageName;
// 语言变化时更新所有翻译文本
ever(KRLanguageUtils.kr_language, (_) {
kr_language.value = KRLanguageUtils.kr_language.value;
_loadThemeOption();
kr_currentCountry.value = "";
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
kr_vpnMode.value = '';
kr_vpnMode.value =
kr_getConnectionTypeString(KRSingBoxImp().kr_connectionType.value);
kr_vpnModeRemark.value = '';
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(KRSingBoxImp().kr_connectionType.value);
});
ever(KRCountryUtil.kr_currentCountry, (_) {
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
});
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
kr_vpnMode.value =
kr_getConnectionTypeString(KRSingBoxImp().kr_connectionType.value);
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(KRSingBoxImp().kr_connectionType.value);
_kr_getVersion();
}
String kr_getConnectionTypeString(KRConnectionType type) {
switch (type) {
case KRConnectionType.global:
return AppTranslations.kr_setting.connectionTypeGlobal;
case KRConnectionType.rule:
return AppTranslations.kr_setting.connectionTypeRule;
// case KRConnectionType.direct:
// return AppTranslations.kr_setting.connectionTypeDirect;
}
}
String kr_getConnectionTypeRemark(KRConnectionType type) {
switch (type) {
case KRConnectionType.global:
return AppTranslations.kr_setting.connectionTypeGlobalRemark;
case KRConnectionType.rule:
return AppTranslations.kr_setting.connectionTypeRuleRemark;
// case KRConnectionType.direct:
// return AppTranslations.kr_setting.connectionTypeDirectRemark;
}
}
void _loadThemeOption() async {
final KRThemeService themeService = KRThemeService();
await themeService.init();
switch (themeService.kr_Theme) {
case ThemeMode.system:
kr_themeOption.value = AppTranslations.kr_setting.system;
break;
case ThemeMode.light:
kr_themeOption.value = AppTranslations.kr_setting.light;
break;
case ThemeMode.dark:
kr_themeOption.value = AppTranslations.kr_setting.dark;
break;
}
}
final count = 0.obs;
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
void increment() => count.value++;
void kr_updateConnectionType(KRConnectionType newType) {
if (KRSingBoxImp().kr_connectionType.value != newType) {
KRLogUtil.kr_i('更新连接类型: $newType', tag: 'SettingController');
KRSingBoxImp().kr_updateConnectionType(newType);
kr_vpnMode.value = kr_getConnectionTypeString(newType);
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(newType);
// 这里可以添加其他需要的逻辑
}
}
// 获取版本号
Future<void> _kr_getVersion() async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
kr_version.value = packageInfo.version;
}
}
@@ -1,493 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../services/singbox_imp/kr_sing_box_imp.dart';
import '../controllers/kr_setting_controller.dart';
import '../../../themes/kr_theme_service.dart';
import '../../../localization/app_translations.dart';
import '../../../routes/app_pages.dart';
import '../../../common/app_run_data.dart';
class KRSettingView extends GetView<KRSettingController> {
const KRSettingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: Text(
AppTranslations.kr_setting.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
body: Obx(() {
return Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15),
Color.fromRGBO(23, 151, 255, 0.05),
],
stops: [0.0, 0.3],
),
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: kToolbarHeight + 20.w),
_kr_buildSectionTitle(
context, AppTranslations.kr_setting.vpnConnection),
_kr_buildVPNSection(context),
_kr_buildSectionTitle(
context, AppTranslations.kr_setting.general),
_kr_buildGeneralSection(context),
SizedBox(height: 100.h),
],
),
),
),
);
}),
);
}
Widget _kr_buildSectionTitle(BuildContext context, String title) {
return Padding(
padding: EdgeInsets.fromLTRB(16.w, 24.h, 16.w, 8.h),
child: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
);
}
Widget _kr_buildVPNSection(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
_kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.mode,
value: controller.kr_vpnMode.value,
// subtitle: controller.kr_vpnModeRemark.value,
onTap: () => _kr_showRouteRuleSelectionSheet(context),
),
_kr_buildDivider(),
// _kr_buildSwitchTile(
// context,
// title: AppTranslations.kr_setting.autoConnect,
// value: controller.kr_autoConnect,
// onChanged: (value) => controller.kr_autoConnect.value = value,
// ),
// _kr_buildDivider(),
// _kr_buildSelectionTile(
// context,
// title: AppTranslations.kr_setting.routeRule,
// value: controller.kr_routeRule.value,
// onTap: () => _kr_showRouteRuleSelectionSheet(context),
// ),
// _kr_buildDivider(),
_kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.countrySelector,
subtitle: AppTranslations.kr_setting.connectionTypeRuleRemark,
value: controller.kr_currentCountry.value,
onTap: () => Get.toNamed(Routes.KR_COUNTRY_SELECTOR),
),
],
),
);
}
void _kr_showVPNModeSelectionSheet(BuildContext context) {
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 16.r),
child: Wrap(
children: [
ListTile(
title: Center(
child: Text(AppTranslations.kr_setting.vpnModeSmart),
),
onTap: () {
controller.kr_changeVPNMode(AppTranslations.kr_setting.vpnModeSmart);
Get.back();
},
),
ListTile(
title: Center(
child: Text(AppTranslations.kr_setting.vpnModeSecure),
),
onTap: () {
controller.kr_changeVPNMode(AppTranslations.kr_setting.vpnModeSecure);
Get.back();
},
),
],
),
),
),
);
}
void _kr_showRouteRuleSelectionSheet(BuildContext context) {
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 16.r),
child: Wrap(
children: KRConnectionType.values.map((type) {
return ListTile(
title: Center(
child: Text(
controller.kr_getConnectionTypeString(type),
style: KrAppTextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
onTap: () {
controller.kr_updateConnectionType(type);
Get.back();
},
);
}).toList(),
),
),
),
);
}
Widget _kr_buildGeneralSection(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
Obx(() => _kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.appearance,
value: controller.kr_themeOption.value,
onTap: () => _showThemeSelectionSheet(context),
)),
_kr_buildDivider(),
_kr_buildSwitchTile(
context,
title: AppTranslations.kr_setting.notifications,
value: controller.kr_notification,
onChanged: (value) => controller.kr_notification.value = value,
),
_kr_buildDivider(),
_kr_buildSwitchTile(
context,
title: AppTranslations.kr_setting.helpImprove,
value: controller.kr_helpImprove,
onChanged: (value) => controller.kr_helpImprove.value = value,
),
_kr_buildDivider(),
Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
if (!isLoggedIn) {
// 未登录,不显示此项
return SizedBox.shrink();
} else if (isDeviceLogin) {
// 设备登录(游客模式),显示"点击这里登录/注册"
return _kr_buildActionTile(
context,
title: 'userInfo.loginRegister'.tr,
trailing: "",
onTap: () => Get.toNamed(Routes.MR_LOGIN),
);
} else {
// 正常登录,显示用户邮箱
final userEmail = appRunData.kr_account.value ?? AppTranslations.kr_userInfo.myAccount;
return _kr_buildActionTile(
context,
title: userEmail,
trailing: AppTranslations.kr_setting.goToDelete,
onTap: controller.kr_deleteAccount,
);
}
}),
_kr_buildDivider(),
// _kr_buildTitleTile(
// context,
// title: AppTranslations.kr_setting.rateUs,
// ),
// _kr_buildDivider(),
Obx(() => _kr_buildValueTile(
context,
title: AppTranslations.kr_setting.version,
value: controller.kr_version.value,
)),
_kr_buildDivider(),
_kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.switchLanguage,
value: controller.kr_language.value,
onTap: controller.kr_changeLanguage,
),
],
),
);
}
void _showThemeSelectionSheet(BuildContext context) {
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 16.r),
child: Wrap(
children: ThemeMode.values.map((option) {
String optionText;
switch (option) {
case ThemeMode.system:
optionText = AppTranslations.kr_setting.system;
break;
case ThemeMode.light:
optionText = AppTranslations.kr_setting.light;
break;
case ThemeMode.dark:
optionText = AppTranslations.kr_setting.dark;
break;
}
return ListTile(
title: Center(
child: Text(
optionText,
style: KrAppTextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
onTap: () async {
final KRThemeService themeService = KRThemeService();
await themeService.kr_switchTheme(option);
controller.kr_themeOption.value = optionText;
Get.back();
},
);
}).toList(),
),
),
),
);
}
Widget _kr_buildSelectionTile(
BuildContext context, {
required String title,
required String value,
String? subtitle,
required VoidCallback onTap,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
subtitle: subtitle != null
? Text(
subtitle,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
)
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(width: 4.w),
Icon(
Icons.arrow_forward_ios,
size: 16.r,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
onTap: onTap,
);
}
Widget _kr_buildSwitchTile(
BuildContext context, {
required String title,
String? subtitle,
required RxBool value,
required Function(bool) onChanged,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
subtitle: subtitle != null
? Text(
subtitle,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
)
: null,
trailing: Obx(
() => CupertinoSwitch(
value: value.value,
onChanged: onChanged,
activeColor: Colors.blue,
),
),
);
}
Widget _kr_buildActionTile(
BuildContext context, {
required String title,
required String trailing,
required VoidCallback onTap,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
trailing: Text(
trailing,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
onTap: onTap,
);
}
Widget _kr_buildTitleTile(
BuildContext context, {
required String title,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
);
}
Widget _kr_buildValueTile(
BuildContext context, {
required String title,
required String value,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
trailing: Text(
value,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
Widget _kr_buildDivider() {
return Divider(
height: 1.h,
thickness: 0.2,
color: const Color(0xFFEEEEEE),
);
}
}
@@ -1,11 +0,0 @@
import 'package:get/get.dart';
import '../controllers/kr_webview_controller.dart';
class KRWebViewBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRWebViewController>(
() => KRWebViewController(),
);
}
}
@@ -1,192 +0,0 @@
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;
import 'package:kaer_with_panels/app/services/api_service/api.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_web_api.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
/// WebView 控制器
/// 用于管理 WebView 的状态和行为
class KRWebViewController extends GetxController {
// 页面加载状态
final RxBool kr_isLoading = true.obs;
// 页面标题
final RxString kr_title = ''.obs;
// WebView 控制器
late final WebViewController kr_webViewController;
// 默认URL
static const String kr_defaultUrl = '';
final String kr_url = Get.arguments['url'] as String;
// Web API 实例
final KRWebApi _kr_webApi = KRWebApi();
// 内容类型
final RxBool kr_isHtml = false.obs;
final RxBool kr_isMarkdown = false.obs;
final RxString kr_content = ''.obs;
@override
void onInit() {
super.onInit();
// 根据 URL 类型决定初始化方式
if (kr_url.contains(Api.kr_getSiteTos) || kr_url.contains(Api.kr_getSitePrivacy)) {
// 用户协议和隐私政策页面,直接获取文本内容
if (kr_url.contains(Api.kr_getSiteTos)) {
kr_title.value = AppTranslations.kr_login.termsOfService;
} else {
kr_title.value = AppTranslations.kr_login.privacyPolicy;
}
kr_getWebText();
} else {
// 其他页面,初始化 WebView
kr_initWebView();
}
}
/// 初始化 WebView
void kr_initWebView() {
kr_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) async {
// 只在移动平台处理支付应用跳转
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
KRLogUtil.kr_i('处理支付链接: ${request.url}', tag: 'WebViewController');
// 处理支付链接
if (await kr_handleUrlLaunch(request.url)) {
return NavigationDecision.prevent;
}
}
return NavigationDecision.navigate;
},
onPageStarted: kr_handlePageStarted,
onPageFinished: kr_handlePageFinished,
),
);
// 检查是否是用户协议或隐私政策
if (kr_url.contains(Api.kr_getSiteTos) || kr_url.contains(Api.kr_getSitePrivacy)) {
kr_getWebText();
} else {
kr_webViewController.loadRequest(Uri.parse(kr_url));
}
}
/// 获取网页文本内容并加载到 WebView
Future<void> kr_getWebText() async {
try {
final response = await _kr_webApi.kr_getWebText(kr_url);
response.fold(
(error) async {
KRLogUtil.kr_e('获取网页内容失败: $error', tag: 'WebViewController');
// 如果获取失败,直接设置错误内容
kr_content.value = 'Failed to load, please try again later';
kr_isLoading.value = false;
},
(content) async {
KRLogUtil.kr_i('获取到内容: $content', tag: 'WebViewController');
// 判断内容类型,优先判断 Markdown
kr_isMarkdown.value = content.contains('**') ||
content.contains('*') ||
content.contains('#') ||
content.contains('- ') ||
content.contains('[');
kr_isHtml.value = !kr_isMarkdown.value && content.contains('<') && content.contains('>');
KRLogUtil.kr_i('内容类型 - Markdown: ${kr_isMarkdown.value}, HTML: ${kr_isHtml.value}', tag: 'WebViewController');
if (kr_isMarkdown.value) {
// 如果是 Markdown 内容,直接使用
kr_content.value = content;
} else if (kr_isHtml.value) {
// 如果是 HTML 内容,直接使用
kr_content.value = content;
} else {
// 如果是普通文本,直接使用
kr_content.value = content;
}
kr_isLoading.value = false;
},
);
} catch (e) {
KRLogUtil.kr_e('获取网页内容出错: $e', tag: 'WebViewController');
kr_content.value = 'Loading error, please try again later';
kr_isLoading.value = false;
}
}
/// 处理页面开始加载事件
void kr_handlePageStarted(String url) {
kr_isLoading.value = true;
}
/// 处理页面加载完成事件
void kr_handlePageFinished(String url) async {
kr_isLoading.value = false;
await kr_updateTitle();
}
/// 更新页面标题
Future<void> kr_updateTitle() async {
final String? kr_pageTitle = await kr_webViewController.getTitle();
kr_title.value = kr_pageTitle ?? '';
}
/// 重新加载页面
Future<void> kr_reloadPage() async {
await kr_webViewController.reload();
}
/// 加载新的URL
Future<void> kr_loadUrl(String url) async {
await kr_webViewController.loadRequest(Uri.parse(url));
}
/// 处理URL启动
Future<bool> kr_handleUrlLaunch(String url) async {
try {
KRLogUtil.kr_i('正在处理URL跳转: $url', tag: 'WebViewController');
final uri = Uri.parse(url);
// 处理支付应用和外部链接
if (uri.scheme == 'alipays' ||
uri.scheme == 'alipay' ||
uri.scheme == 'weixin' ||
uri.scheme == 'wx') {
KRLogUtil.kr_i('检测到支付应用scheme: ${uri.scheme}', tag: 'WebViewController');
// 尝试打开支付应用
if (await canLaunchUrl(uri)) {
return await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
}
// 如果支付应用无法打开,尝试使用外部浏览器打开
final httpUri = Uri.parse('https://${uri.host}${uri.path}?${uri.query}');
KRLogUtil.kr_i('尝试使用浏览器打开: $httpUri', tag: 'WebViewController');
if (await canLaunchUrl(httpUri)) {
return await launchUrl(
httpUri,
mode: LaunchMode.externalApplication,
);
}
KRLogUtil.kr_e('无法启动URL: $url', tag: 'WebViewController');
}
return false;
} catch (e) {
KRLogUtil.kr_e('URL跳转错误: $e', tag: 'WebViewController');
return false;
}
}
}
@@ -1,177 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../../../widgets/kr_app_text_style.dart';
import '../controllers/kr_webview_controller.dart';
import '../../../services/api_service/api.dart';
import '../../../utils/kr_log_util.dart';
/// WebView 页面组件
class KRWebView extends GetView<KRWebViewController> {
const KRWebView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: Text(
controller.kr_title.value,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
body: _buildBody(),
);
}
/// 构建主体内容
Widget _buildBody() {
return Stack(
children: [
_buildContent(),
_buildLoadingIndicator(),
],
);
}
/// 构建内容组件
Widget _buildContent() {
if (controller.kr_url.contains(Api.kr_getSiteTos) ||
controller.kr_url.contains(Api.kr_getSitePrivacy)) {
return _buildProtocolContent();
} else {
return _buildWebView();
}
}
/// 构建协议内容
Widget _buildProtocolContent() {
return Obx(() {
if (controller.kr_isHtml.value) {
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Html(
data: controller.kr_content.value,
style: {
'body': Style(
margin: Margins.all(0),
padding: HtmlPaddings.all(0),
fontSize: FontSize(14.sp),
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
lineHeight: LineHeight(1.4),
),
'p': Style(
margin: Margins.only(bottom: 8.h),
),
'b': Style(
fontWeight: FontWeight.bold,
),
'i': Style(
fontStyle: FontStyle.italic,
),
'a': Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
),
},
shrinkWrap: true,
),
);
} else if (controller.kr_isMarkdown.value) {
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: MarkdownBody(
data: controller.kr_content.value,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
fontSize: 14.sp,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
strong: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
em: TextStyle(
fontSize: 14.sp,
fontStyle: FontStyle.italic,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
a: TextStyle(
fontSize: 14.sp,
color: Colors.blue,
decoration: TextDecoration.underline,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
),
),
);
} else {
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Text(
controller.kr_content.value,
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
),
);
}
});
}
/// 构建 WebView 组件
Widget _buildWebView() {
return WebViewWidget(
controller: controller.kr_webViewController,
);
}
/// 构建加载指示器
Widget _buildLoadingIndicator() {
return Obx(
() => controller.kr_isLoading.value
? const Center(
child: CircularProgressIndicator(),
)
: const SizedBox.shrink(),
);
}
/// 显示错误提示
void _showErrorSnackbar(String title, String message) {
Get.snackbar(
title,
message,
snackPosition: SnackPosition.BOTTOM,
);
}
}
+5 -45
View File
@@ -14,6 +14,7 @@ import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import 'package:kaer_with_panels/app/utils/kr_http_adapter_util.dart';
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
// import 'package:crypto/crypto.dart';
@@ -74,52 +75,11 @@ class HttpUtil {
};
// 🔧 配置HttpClientAdapter 优先走本地 sing-box mixed 端口,
// 若代理不可用则回退到直连
// 若代理不可用则回到直连
KRLogUtil.kr_i('🔧 配置 HttpClientAdapter...', tag: 'HttpUtil');
_dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
KRLogUtil.kr_i('📱 createHttpClient 回调被调用', tag: 'HttpUtil');
final client = HttpClient();
// ✅ 优化:智能代理回退逻辑
client.findProxy = (url) {
try {
// 检查 SingBox 是否正在运行
final singBoxStatus = KRSingBoxImp.instance.kr_status;
final isProxyAvailable = singBoxStatus == SingboxStatus.started();
if (!isProxyAvailable) {
// 代理未运行,直接使用直连
KRLogUtil.kr_i(
'🔄 代理未运行,使用直连模式: $url',
tag: 'HttpUtil',
);
return 'DIRECT';
}
// 代理正在运行,使用代理配置
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
KRLogUtil.kr_i(
'✅ 使用代理模式, url: $url, proxy: $proxyConfig',
tag: 'HttpUtil',
);
return proxyConfig;
} catch (e) {
// 发生异常时回退到直连
KRLogUtil.kr_w(
'⚠️ 代理配置异常,回退到直连: $e',
tag: 'HttpUtil',
);
return 'DIRECT';
}
};
// ✅ 优化:设置连接失败时的自动回退
client.connectionTimeout = const Duration(seconds: 10);
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
_dio.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
useSingBoxProxy: true,
timeout: const Duration(seconds: 10),
);
KRLogUtil.kr_i('✅ HttpUtil.initDio() 初始化完成', tag: 'HttpUtil');
}
-40
View File
@@ -8,10 +8,6 @@ import 'package:kaer_with_panels/app/modules/hi_node_list/views/hi_page_node_vie
import 'package:kaer_with_panels/app/modules/hi_user_info/bindings/hi_user_info_binding.dart';
import 'package:kaer_with_panels/app/modules/hi_user_info/views/hi_user_info_view.dart';
import '../modules/kr_language_selector/bindings/kr_language_selector_binding.dart';
import '../modules/kr_language_selector/views/kr_language_selector_view.dart';
import '../modules/kr_country_selector/bindings/kr_country_selector_binding.dart';
import '../modules/kr_country_selector/views/kr_country_selector_view.dart';
import '../modules/kr_crisp_chat/bindings/kr_crisp_binding.dart';
import '../modules/kr_crisp_chat/views/kr_crisp_view.dart';
import '../modules/kr_delete_account/bindings/kr_delete_account_binding.dart';
@@ -26,16 +22,10 @@ import '../modules/kr_message/bindings/kr_message_binding.dart';
import '../modules/kr_message/views/kr_message_view.dart';
import '../modules/kr_purchase_membership/bindings/kr_purchase_membership_binding.dart';
import '../modules/kr_purchase_membership/views/kr_purchase_membership_view.dart';
import '../modules/kr_setting/bindings/kr_setting_binding.dart';
import '../modules/kr_setting/views/kr_setting_view.dart';
import '../modules/kr_webview/bindings/kr_webview_binding.dart';
import '../modules/kr_webview/views/kr_webview_view.dart';
import '../modules/kr_order_status/bindings/kr_order_status_binding.dart';
import '../modules/kr_order_status/views/kr_order_status_view.dart';
import '../modules/kr_splash/bindings/kr_splash_binding.dart';
import '../modules/kr_splash/views/kr_splash_view.dart';
import '../modules/kr_device_management/bindings/kr_device_management_binding.dart';
import '../modules/kr_device_management/views/kr_device_management_view.dart';
import 'package:kaer_with_panels/app/routes/transitions/slide_transparent_transition.dart';
import 'package:kaer_with_panels/app/widgets/swipe/swipe_wrapper.dart';
@@ -84,30 +74,12 @@ class AppPages {
popGesture: false,
arguments: {'showSubscriptionButton': true}, // 显示购买按钮
),
GetPage(
name: _Paths.KR_SETTING,
page: () => SwipeWrapper.detect(() => const KRSettingView()),
binding: KRSettingBinding(),
popGesture: false,
),
GetPage(
name: _Paths.KR_INVITE,
page: () => SwipeWrapper.detect(() => const KRInviteView()),
binding: KRInviteBinding(),
popGesture: false,
),
GetPage(
name: _Paths.KR_LANGUAGE_SELECTOR,
page: () => SwipeWrapper.detect(() => const KRLanguageSelectorView()),
binding: KRLanguageSelectorBinding(),
popGesture: false,
),
GetPage(
name: _Paths.KR_COUNTRY_SELECTOR,
page: () => SwipeWrapper.detect(() => const KRCountrySelectorView()),
binding: KRCountrySelectorBinding(),
popGesture: false,
),
GetPage(
name: _Paths.KR_PURCHASE_MEMBERSHIP,
page: () => SwipeWrapper.detect(() => const KRPurchaseMembershipView()),
@@ -127,12 +99,6 @@ class AppPages {
binding: KrDeleteAccountBinding(),
popGesture: false,
),
GetPage(
name: Routes.KR_WEBVIEW,
page: () => SwipeWrapper.detect(() => const KRWebView()),
binding: KRWebViewBinding(),
popGesture: false,
),
GetPage(
name: Routes.KR_ORDER_STATUS,
page: () => SwipeWrapper.detect(() => const KROrderStatusView()),
@@ -145,12 +111,6 @@ class AppPages {
binding: KRCrispBinding(),
popGesture: false,
),
GetPage(
name: _Paths.KR_DEVICE_MANAGEMENT,
page: () => SwipeWrapper.detect(() => const KRDeviceManagementView()),
binding: KRDeviceManagementBinding(),
popGesture: false,
),
GetPage(
name: _Paths.HI_NODE_LIST,
page: () => SwipeWrapper.detect(() => const HINodePageView()),
-2
View File
@@ -17,7 +17,6 @@ abstract class Routes {
static const KR_PURCHASE_MEMBERSHIP = _Paths.KR_PURCHASE_MEMBERSHIP;
static const KR_MESSAGE = _Paths.KR_MESSAGE;
static const KR_DELETE_ACCOUNT = _Paths.KR_DELETE_ACCOUNT;
static const KR_WEBVIEW = _Paths.KR_WEBVIEW;
static const KR_ORDER_STATUS = '/kr-order-status';
static const KR_CRISP = _Paths.KR_CRISP;
static const KR_DEVICE_MANAGEMENT = _Paths.KR_DEVICE_MANAGEMENT;
@@ -43,7 +42,6 @@ abstract class _Paths {
static const KR_PURCHASE_MEMBERSHIP = '/kr-purchase-membership';
static const KR_MESSAGE = '/kr-message';
static const KR_DELETE_ACCOUNT = '/kr-delete-account';
static const KR_WEBVIEW = '/kr_webview';
static const KR_CRISP = '/kr-crisp';
static const KR_DEVICE_MANAGEMENT = '/kr-device-management';
static const HI_MENU = '/hi_menu';
@@ -22,6 +22,7 @@ import '../kr_site_config_service.dart';
import '../../common/app_config.dart';
import 'package:dio/dio.dart' as dio;
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/utils/kr_http_adapter_util.dart';
import 'package:flutter/foundation.dart';
class KRAuthApi {
@@ -401,6 +402,13 @@ class KRAuthApi {
// 使用 Dio 直接发送请求(因为需要特殊的加密处理)
final dioInstance = dio.Dio();
// ✅ 关键修复:统一使用配置好的 Adapter,包含 SSL 验证绕过和代理处理
dioInstance.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
useSingBoxProxy: false, // 设备登录建议先直连
forceDirect: true,
timeout: const Duration(seconds: 10),
);
final baseUrl = AppConfig.getInstance().baseUrl;
final url = '$baseUrl${Api.kr_deviceLogin}';
@@ -229,8 +229,11 @@ class KRSubscribeApi {
/// 获取用户已订阅套餐
Future<Either<HttpError, List<KRAlreadySubscribe>>>
kr_getAlreadySubscribe() async {
kr_getAlreadySubscribe({String? includeExpired}) async {
final Map<String, dynamic> data = <String, dynamic>{};
if (includeExpired != null) {
data['includeExpired'] = includeExpired;
}
BaseResponse<KRAlreadySubscribeList> baseResponse =
await HttpUtil.getInstance().request<KRAlreadySubscribeList>(
+57 -14
View File
@@ -3,6 +3,8 @@ import 'package:get/get.dart';
import '../modules/kr_home/views/hi_subscription_corner_button.dart';
import 'package:kaer_with_panels/main.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
class GlobalOverlayService extends GetxService {
Color? _currentColor;
@@ -24,9 +26,14 @@ class GlobalOverlayService extends GetxService {
}
OverlayEntry? _overlayEntry;
final RxBool _isVisible = false.obs;
final RxString _currentRoute = ''.obs;
bool get isVisible => _isVisible.value;
void updateCurrentRoute(String route) {
_currentRoute.value = route;
}
@override
void onInit() {
super.onInit();
@@ -77,20 +84,56 @@ class GlobalOverlayService extends GetxService {
// 2 money-icon
Positioned(
top: MediaQuery.of(context).padding.top + 8,
right: ((radius - (statusBarHeight / 2)) / 2) + 3,
child: GestureDetector(
behavior: HitTestBehavior.translucent, //
onTap: () {
//
print('🔥 money-icon tapped → trigger HISubscriptionCornerButton animation');
GlobalOverlayService.instance.triggerSubscriptionAnimation();
},
child: IgnorePointer(
ignoring: false, //
child: KrLocalImage(
imageName: 'money-icon',
imageType: ImageType.svg,
top: MediaQuery.of(context).padding.top,
right: (radius / 2),
child: FractionalTranslation(
// Offset x y
// 0.5 50%-0.5 50%
translation: const Offset(0.5, 0),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
print('🔥 money-icon tapped');
GlobalOverlayService.instance.triggerSubscriptionAnimation();
},
child: IgnorePointer(
ignoring: false,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: 'money-icon',
imageType: ImageType.svg,
width: 32.w, //
),
SizedBox(height: 2.w),
Obx(() {
final current = _currentRoute.value;
print('🔥 GlobalOverlayService Obx: currentRoute = $current');
// 1. 2. 3.
bool isHidden = _currentColor == Colors.transparent ||
current == Routes.KR_PURCHASE_MEMBERSHIP ||
current.contains(Routes.KR_PURCHASE_MEMBERSHIP) ||
Routes.KR_PURCHASE_MEMBERSHIP.contains(current) ||
current == Routes.KR_ORDER_STATUS ||
current.contains('purchase-membership');
if (isHidden) {
return const SizedBox.shrink();
}
return Text(
'购买套餐',
style: TextStyle(
color: Colors.black,
fontSize: 12.sp,
),
);
}),
],
),
),
),
),
+8 -4
View File
@@ -225,17 +225,21 @@ class KRDeviceInfoService {
}
/// Windows设备ID - 使GUID
/// 🔧 使 flutter_udid wmic
Future<String> _getWindowsDeviceId() async {
try {
final windowsInfo = await _deviceInfo.windowsInfo;
// 使 flutter_udid
String udid = await FlutterUdid.consistentUdid;
// 🔧 使 FlutterUdid.consistentUdid
// Windows "cmd.exe /c wmic csproduct get UUID"
// 使 CREATE_NO_WINDOW
// 使 device_info_plus
// windowsInfo.deviceId
//
final factors = [
udid,
windowsInfo.deviceId, // ID
windowsInfo.deviceId, // ID ( MachineGuid)
windowsInfo.computerName, //
windowsInfo.productName, //
windowsInfo.numberOfCores.toString(), // CPU核心数
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import '../model/response/kr_site_config.dart';
import '../common/app_config.dart';
import '../utils/kr_log_util.dart';
import '../utils/kr_http_adapter_util.dart';
import 'singbox_imp/kr_sing_box_imp.dart';
///
@@ -25,6 +26,14 @@ class KRSiteConfigService extends ChangeNotifier {
'🌐 网站配置服务:使用直连模式(不通过代理)',
tag: 'KRSiteConfigService',
);
// 🔧 SSL
_dio.httpClientAdapter = KRHttpAdapterUtil.createAdapter(
useSingBoxProxy: false,
forceDirect: true,
timeout: const Duration(seconds: 20),
);
if (kDebugMode) {
print('🌐 网站配置服务:使用直连模式,避免 SingBox 未初始化问题');
}
+47
View File
@@ -0,0 +1,47 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRHttpAdapterUtil {
/// HttpClientAdapter
///
/// [useSingBoxProxy] sing-box true
/// [forceDirect] 使 false
static HttpClientAdapter createAdapter({
bool useSingBoxProxy = true,
bool forceDirect = false,
Duration timeout = const Duration(seconds: 10),
}) {
return IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.connectionTimeout = timeout;
// SSL
client.badCertificateCallback = (cert, host, port) => true;
if (forceDirect) {
client.findProxy = (url) {
KRLogUtil.kr_i('🔍 请求使用直连: $url', tag: 'KRHttpAdapterUtil');
return 'DIRECT';
};
} else if (useSingBoxProxy) {
client.findProxy = (url) {
try {
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
KRLogUtil.kr_i('🔍 请求使用代理: $proxyConfig, url: $url', tag: 'KRHttpAdapterUtil');
return proxyConfig;
} catch (e) {
KRLogUtil.kr_w('⚠️ 获取代理配置异常,回退到 DIRECT: $e', tag: 'KRHttpAdapterUtil');
return 'DIRECT';
}
};
}
return client;
},
);
}
}
+11 -6
View File
@@ -25,8 +25,9 @@ class KRWindowManager with WindowListener, TrayListener {
KRLogUtil.kr_i('kr_initWindowManager: 窗口管理器已初始化');
const WindowOptions windowOptions = WindowOptions(
size: Size(800, 668),
minimumSize: Size(400, 334),
size: Size(420, 800),
minimumSize: Size(420, 800),
maximumSize: Size(420, 800),
center: true,
backgroundColor: Colors.white,
skipTaskbar: false,
@@ -46,16 +47,20 @@ class KRWindowManager with WindowListener, TrayListener {
if (Platform.isWindows) {
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
await windowManager.setTitle('HiFastVPN');
await windowManager.setSize(const Size(800, 668));
await windowManager.setMinimumSize(const Size(400, 334));
await windowManager.setSize(const Size(420, 800));
await windowManager.setMinimumSize(const Size(420, 800));
await windowManager.setMaximumSize(const Size(420, 800));
await windowManager.setResizable(false);
await windowManager.center();
await windowManager.show();
//
await windowManager.setPreventClose(true);
} else {
await windowManager.setTitle('HiFastVPN');
await windowManager.setSize(const Size(800, 668));
await windowManager.setMinimumSize(const Size(400, 334));
await windowManager.setSize(const Size(420, 800));
await windowManager.setMinimumSize(const Size(420, 800));
await windowManager.setMaximumSize(const Size(420, 800));
await windowManager.setResizable(false);
await windowManager.center();
}
+123 -38
View File
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/utils/kr_windows_process_util.dart';
/// Windows DNS
///
@@ -38,28 +39,48 @@ class KRWindowsDnsUtil {
}
try {
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
// 🔒 5
return await Future.value(() async {
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
// 1.
final interfaceName = await _kr_getPrimaryNetworkInterface();
if (interfaceName == null) {
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
return false;
}
_primaryInterfaceName = interfaceName;
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
// 1.
final interfaceName = await _kr_getPrimaryNetworkInterface();
if (interfaceName == null) {
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
return false;
}
_primaryInterfaceName = interfaceName;
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
// 2. DNS
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
if (dnsServers.isEmpty) {
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS');
_originalDnsServers = []; // DHCP
} else {
_originalDnsServers = dnsServers;
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
}
// 2. DNS
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
return true;
// 🔧 P0修复1: 127.0.0.1 (sing-box DNS)
// 127.0.0.1 VPN 127.0.0.1 sing-box DNS
final validDnsServers = dnsServers.where((dns) => !dns.startsWith('127.')).toList();
if (validDnsServers.isEmpty) {
KRLogUtil.kr_w('⚠️ 当前 DNS 为空或全是本地地址,设为 DHCP 自动获取', tag: 'WindowsDNS');
if (dnsServers.isNotEmpty) {
KRLogUtil.kr_i(' (已过滤的本地DNS: ${dnsServers.join(", ")})', tag: 'WindowsDNS');
}
_originalDnsServers = []; // DHCP
} else {
_originalDnsServers = validDnsServers;
KRLogUtil.kr_i('✅ 已备份有效 DNS: ${validDnsServers.join(", ")}', tag: 'WindowsDNS');
if (dnsServers.length != validDnsServers.length) {
KRLogUtil.kr_i(' (已过滤掉 ${dnsServers.length - validDnsServers.length} 个本地地址)', tag: 'WindowsDNS');
}
}
return true;
}()).timeout(
const Duration(seconds: 5),
onTimeout: () {
KRLogUtil.kr_w('⏱️ DNS 备份操作超时(5秒),跳过备份', tag: 'WindowsDNS');
return false;
},
);
} catch (e) {
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
return false;
@@ -80,22 +101,33 @@ class KRWindowsDnsUtil {
try {
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
// 1.
if (_primaryInterfaceName == null) {
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS');
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
if (_primaryInterfaceName == null) {
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
// 🔧 P1修复:
final currentInterface = await _kr_getPrimaryNetworkInterface();
if (currentInterface == null) {
KRLogUtil.kr_e('❌ 无法检测当前网络接口,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
// 2. DNS
//
if (_primaryInterfaceName != null && _primaryInterfaceName != currentInterface) {
KRLogUtil.kr_w('⚠️ 网络接口已变化: $_primaryInterfaceName$currentInterface', tag: 'WindowsDNS');
KRLogUtil.kr_w(' 执行兜底恢复以确保当前接口DNS正常', tag: 'WindowsDNS');
_primaryInterfaceName = currentInterface; //
return await _kr_fallbackRestoreDns();
}
// 使
_primaryInterfaceName = currentInterface;
KRLogUtil.kr_i('🔍 当前网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
// 1. DNS
if (_originalDnsServers == null) {
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
// 2. DNS
if (_originalDnsServers!.isEmpty) {
// DHCP
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
@@ -130,6 +162,15 @@ class KRWindowsDnsUtil {
return await _kr_fallbackRestoreDns();
}
// 🔧 P2优化: DNS
KRLogUtil.kr_i('🧪 测试 DNS 解析功能...', tag: 'WindowsDNS');
final canResolve = await _kr_testDnsResolution();
if (!canResolve) {
KRLogUtil.kr_w('⚠️ DNS 解析测试失败,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
KRLogUtil.kr_i('✅ DNS 解析测试通过', tag: 'WindowsDNS');
return true;
} catch (e) {
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
@@ -187,7 +228,7 @@ class KRWindowsDnsUtil {
Future<String?> _kr_getPrimaryNetworkInterface() async {
try {
// 使 netsh
final result = await Process.run('netsh', ['interface', 'show', 'interface']);
final result = await KRWindowsProcessUtil.runHidden('netsh', ['interface', 'show', 'interface']);
if (result.exitCode != 0) {
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
@@ -271,7 +312,7 @@ class KRWindowsDnsUtil {
/// DNS
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
try {
final result = await Process.run('netsh', [
final result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface',
'ipv4',
'show',
@@ -294,10 +335,9 @@ class KRWindowsDnsUtil {
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
if (ipMatch != null) {
final ip = ipMatch.group(0)!;
//
if (!ip.startsWith('127.')) {
dnsServers.add(ip);
}
// 🔧 127.0.0.1便DNS是否还在使用sing-box的本地DNS
// DNS时126127.0.0.1
dnsServers.add(ip);
}
}
@@ -325,7 +365,7 @@ class KRWindowsDnsUtil {
// 1. DNS
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
var result = await Process.run('netsh', [
var result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface',
'ipv4',
'set',
@@ -345,7 +385,7 @@ class KRWindowsDnsUtil {
if (dnsServers.length > 1) {
for (int i = 1; i < dnsServers.length; i++) {
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
result = await Process.run('netsh', [
result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface',
'ipv4',
'add',
@@ -384,7 +424,7 @@ class KRWindowsDnsUtil {
try {
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
final result = await Process.run('netsh', [
final result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface',
'ipv4',
'set',
@@ -416,7 +456,7 @@ class KRWindowsDnsUtil {
try {
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
final result = await Process.run('ipconfig', ['/flushdns']);
final result = await KRWindowsProcessUtil.runHidden('ipconfig', ['/flushdns']);
if (result.exitCode == 0) {
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
@@ -428,6 +468,51 @@ class KRWindowsDnsUtil {
}
}
/// 🔧 P2优化: DNS
///
/// nslookup
/// true DNS false DNS
Future<bool> _kr_testDnsResolution() async {
try {
//
final testDomains = ['www.baidu.com', 'www.qq.com', 'dns.alidns.com'];
for (var domain in testDomains) {
try {
// 使 nslookup DNS 2
final result = await KRWindowsProcessUtil.runHidden(
'nslookup',
[domain],
).timeout(
const Duration(seconds: 2),
onTimeout: () {
return ProcessResult(0, 1, '', 'Timeout');
},
);
if (result.exitCode == 0) {
final output = result.stdout.toString();
// IP
if (output.contains('Address:') || output.contains('地址:')) {
KRLogUtil.kr_i('✅ DNS 解析测试通过: $domain', tag: 'WindowsDNS');
return true;
}
}
} catch (e) {
//
continue;
}
}
//
KRLogUtil.kr_w('⚠️ 所有测试域名解析均失败', tag: 'WindowsDNS');
return false;
} catch (e) {
KRLogUtil.kr_e('❌ DNS 解析测试异常: $e', tag: 'WindowsDNS');
return false;
}
}
///
///
/// 退
+594
View File
@@ -0,0 +1,594 @@
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
class KRWindowsProcessUtil {
/// 🔍 true
static const bool _debugCommandExecution = true; //
static Future<ProcessResult> runHidden(String executable, List<String> arguments) async {
if (!Platform.isWindows) {
return Process.run(executable, arguments);
}
final result = await _runHiddenWindows(executable, arguments);
return result;
}
static Future<int> startHidden(String executable, List<String> arguments) async {
if (!Platform.isWindows) {
final process = await Process.start(executable, arguments);
return process.pid;
}
return _startHiddenWindows(executable, arguments);
}
static String _buildCommandLine(String executable, List<String> arguments) {
final parts = <String>[_quoteArgument(executable)];
for (final arg in arguments) {
parts.add(_quoteArgument(arg));
}
return parts.join(' ');
}
static bool _shouldSearchPath(String executable) {
if (executable.isEmpty) {
return true;
}
return !(executable.contains('\\') || executable.contains('/') || executable.contains(':'));
}
static String _quoteArgument(String value) {
if (value.isEmpty) {
return '""';
}
final needsQuotes = value.contains(' ') || value.contains('\t') || value.contains('"');
if (!needsQuotes) {
return value;
}
final buffer = StringBuffer('"');
var backslashes = 0;
for (var i = 0; i < value.length; i++) {
final char = value[i];
if (char == '\\') {
backslashes++;
continue;
}
if (char == '"') {
buffer.write('\\' * (backslashes * 2 + 1));
buffer.write('"');
backslashes = 0;
continue;
}
if (backslashes > 0) {
buffer.write('\\' * backslashes);
backslashes = 0;
}
buffer.write(char);
}
if (backslashes > 0) {
buffer.write('\\' * (backslashes * 2));
}
buffer.write('"');
return buffer.toString();
}
static Future<ProcessResult> _runHiddenWindows(String executable, List<String> arguments) async {
final stdoutPipe = _createPipe();
final stderrPipe = _createPipe();
final startupInfo = calloc<STARTUPINFO>();
final processInfo = calloc<PROCESS_INFORMATION>();
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
final stdInput = _getStdInputHandle();
startupInfo.ref
..cb = sizeOf<STARTUPINFO>()
..dwFlags = STARTF_USESTDHANDLES
..hStdInput = stdInput
..hStdOutput = stdoutPipe.write
..hStdError = stderrPipe.write;
final created = _CreateProcessW(
applicationName,
commandLine,
nullptr,
nullptr,
TRUE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
startupInfo,
processInfo,
);
calloc.free(commandLine);
if (applicationName != nullptr) {
calloc.free(applicationName);
}
if (created == 0) {
_closeHandle(stdoutPipe.read);
_closeHandle(stdoutPipe.write);
_closeHandle(stderrPipe.read);
_closeHandle(stderrPipe.write);
calloc.free(startupInfo);
calloc.free(processInfo);
throw Exception('CreateProcessW failed: ${_GetLastError()}');
}
_closeHandle(stdoutPipe.write);
_closeHandle(stderrPipe.write);
final output = await _collectOutput(processInfo.ref.hProcess, stdoutPipe.read, stderrPipe.read);
final exitCode = _getExitCode(processInfo.ref.hProcess);
_closeHandle(stdoutPipe.read);
_closeHandle(stderrPipe.read);
_closeHandle(processInfo.ref.hThread);
_closeHandle(processInfo.ref.hProcess);
final pid = processInfo.ref.dwProcessId;
calloc.free(startupInfo);
calloc.free(processInfo);
return ProcessResult(pid, exitCode, output.stdout, output.stderr);
}
static Future<int> _startHiddenWindows(String executable, List<String> arguments) async {
final startupInfo = calloc<STARTUPINFO>();
final processInfo = calloc<PROCESS_INFORMATION>();
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
startupInfo.ref.cb = sizeOf<STARTUPINFO>();
final created = _CreateProcessW(
applicationName,
commandLine,
nullptr,
nullptr,
FALSE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
startupInfo,
processInfo,
);
calloc.free(commandLine);
if (applicationName != nullptr) {
calloc.free(applicationName);
}
if (created == 0) {
calloc.free(startupInfo);
calloc.free(processInfo);
throw Exception('CreateProcessW failed: ${_GetLastError()}');
}
final pid = processInfo.ref.dwProcessId;
_closeHandle(processInfo.ref.hThread);
_closeHandle(processInfo.ref.hProcess);
calloc.free(startupInfo);
calloc.free(processInfo);
return pid;
}
static _Pipe _createPipe() {
final readHandle = calloc<Pointer<Void>>();
final writeHandle = calloc<Pointer<Void>>();
final securityAttributes = calloc<SECURITY_ATTRIBUTES>();
securityAttributes.ref
..nLength = sizeOf<SECURITY_ATTRIBUTES>()
..bInheritHandle = TRUE
..lpSecurityDescriptor = nullptr;
final created = _CreatePipe(readHandle, writeHandle, securityAttributes, 0);
calloc.free(securityAttributes);
if (created == 0) {
calloc.free(readHandle);
calloc.free(writeHandle);
throw Exception('CreatePipe failed: ${_GetLastError()}');
}
final readValue = readHandle.value;
final writeValue = writeHandle.value;
calloc.free(readHandle);
calloc.free(writeHandle);
final infoResult = _SetHandleInformation(readValue, HANDLE_FLAG_INHERIT, 0);
if (infoResult == 0) {
_closeHandle(readValue);
_closeHandle(writeValue);
throw Exception('SetHandleInformation failed: ${_GetLastError()}');
}
return _Pipe(readValue, writeValue);
}
static Pointer<Void> _getStdInputHandle() {
final handle = _GetStdHandle(STD_INPUT_HANDLE);
if (handle == INVALID_HANDLE_VALUE || handle == 0) {
return nullptr;
}
return Pointer<Void>.fromAddress(handle);
}
static Future<_ProcessOutput> _collectOutput(
Pointer<Void> process,
Pointer<Void> stdoutHandle,
Pointer<Void> stderrHandle,
) async {
final stdoutBuilder = BytesBuilder();
final stderrBuilder = BytesBuilder();
while (true) {
final stdoutRead = _drainPipe(stdoutHandle, stdoutBuilder);
final stderrRead = _drainPipe(stderrHandle, stderrBuilder);
final waitResult = _WaitForSingleObject(process, 0);
if (waitResult == WAIT_OBJECT_0) {
break;
}
if (waitResult == WAIT_FAILED) {
throw Exception('WaitForSingleObject failed: ${_GetLastError()}');
}
if (!stdoutRead && !stderrRead) {
await Future.delayed(const Duration(milliseconds: 10));
} else {
await Future<void>.delayed(Duration.zero);
}
}
while (_drainPipe(stdoutHandle, stdoutBuilder) || _drainPipe(stderrHandle, stderrBuilder)) {
await Future<void>.delayed(Duration.zero);
}
return _ProcessOutput(
_decodeOutput(stdoutBuilder),
_decodeOutput(stderrBuilder),
);
}
static bool _drainPipe(Pointer<Void> handle, BytesBuilder builder) {
final buffer = calloc<Uint8>(4096);
final bytesRead = calloc<Uint32>();
final available = calloc<Uint32>();
var didRead = false;
while (true) {
final peekOk = _PeekNamedPipe(handle, nullptr, 0, nullptr, available, nullptr);
if (peekOk == 0 || available.value == 0) {
break;
}
final toRead = available.value < 4096 ? available.value : 4096;
final ok = _ReadFile(handle, buffer.cast<Void>(), toRead, bytesRead, nullptr);
final read = ok == 0 ? 0 : bytesRead.value;
if (read == 0) {
break;
}
builder.add(buffer.asTypedList(read));
didRead = true;
}
calloc.free(buffer);
calloc.free(bytesRead);
calloc.free(available);
return didRead;
}
static String _decodeOutput(BytesBuilder builder) {
if (builder.length == 0) {
return '';
}
final bytes = builder.toBytes();
try {
return systemEncoding.decode(bytes);
} catch (_) {
return utf8.decode(bytes, allowMalformed: true);
}
}
static int _getExitCode(Pointer<Void> process) {
final exitCode = calloc<Uint32>();
final ok = _GetExitCodeProcess(process, exitCode);
final code = ok == 0 ? -1 : exitCode.value;
calloc.free(exitCode);
return code;
}
static void _closeHandle(Pointer<Void> handle) {
if (handle == nullptr) {
return;
}
_CloseHandle(handle);
}
// 🔧 WinINet API helpers for proxy settings
///
static String? queryWindowsProxyServer() {
if (!Platform.isWindows) return null;
try {
final bufferSize = calloc<Uint32>();
bufferSize.value = sizeOf<INTERNET_PROXY_INFO>();
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
final result = _InternetQueryOptionW(nullptr, INTERNET_OPTION_PROXY, proxyInfo.cast<Void>(), bufferSize);
if (result == 0) {
calloc.free(bufferSize);
calloc.free(proxyInfo);
return null;
}
final proxyServer = proxyInfo.ref.lpszProxy.toDartString();
calloc.free(bufferSize);
calloc.free(proxyInfo);
return proxyServer.isEmpty ? null : proxyServer;
} catch (e) {
return null;
}
}
///
static bool setWindowsProxyServer(String? server) {
if (!Platform.isWindows) return false;
try {
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
if (server != null && server.isNotEmpty) {
//
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
proxyInfo.ref.lpszProxy = server.toNativeUtf16();
proxyInfo.ref.lpszProxyBypass = ''.toNativeUtf16();
} else {
//
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_DIRECT;
proxyInfo.ref.lpszProxy = nullptr;
proxyInfo.ref.lpszProxyBypass = nullptr;
}
final result = _InternetSetOptionW(
nullptr,
INTERNET_OPTION_PROXY,
proxyInfo.cast<Void>(),
sizeOf<INTERNET_PROXY_INFO>(),
);
if (server != null && server.isNotEmpty) {
calloc.free(proxyInfo.ref.lpszProxy);
calloc.free(proxyInfo.ref.lpszProxyBypass);
}
calloc.free(proxyInfo);
return result != 0;
} catch (e) {
return false;
}
}
///
static bool disableWindowsProxy() {
return setWindowsProxyServer(null);
}
}
class _Pipe {
final Pointer<Void> read;
final Pointer<Void> write;
_Pipe(this.read, this.write);
}
class _ProcessOutput {
final String stdout;
final String stderr;
_ProcessOutput(this.stdout, this.stderr);
}
const int TRUE = 1;
const int FALSE = 0;
const int STARTF_USESTDHANDLES = 0x00000100;
const int CREATE_NO_WINDOW = 0x08000000;
const int HANDLE_FLAG_INHERIT = 0x00000001;
const int WAIT_OBJECT_0 = 0x00000000;
const int WAIT_FAILED = 0xFFFFFFFF;
const int STD_INPUT_HANDLE = -10;
const int INVALID_HANDLE_VALUE = -1;
final class SECURITY_ATTRIBUTES extends Struct {
@Uint32()
external int nLength;
external Pointer<Void> lpSecurityDescriptor;
@Int32()
external int bInheritHandle;
}
final class STARTUPINFO extends Struct {
@Uint32()
external int cb;
external Pointer<Utf16> lpReserved;
external Pointer<Utf16> lpDesktop;
external Pointer<Utf16> lpTitle;
@Uint32()
external int dwX;
@Uint32()
external int dwY;
@Uint32()
external int dwXSize;
@Uint32()
external int dwYSize;
@Uint32()
external int dwXCountChars;
@Uint32()
external int dwYCountChars;
@Uint32()
external int dwFillAttribute;
@Uint32()
external int dwFlags;
@Uint16()
external int wShowWindow;
@Uint16()
external int cbReserved2;
external Pointer<Uint8> lpReserved2;
external Pointer<Void> hStdInput;
external Pointer<Void> hStdOutput;
external Pointer<Void> hStdError;
}
final class PROCESS_INFORMATION extends Struct {
external Pointer<Void> hProcess;
external Pointer<Void> hThread;
@Uint32()
external int dwProcessId;
@Uint32()
external int dwThreadId;
}
final DynamicLibrary _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreatePipe = _kernel32.lookupFunction<
Int32 Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, Uint32),
int Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, int)>(
'CreatePipe',
);
final _SetHandleInformation = _kernel32.lookupFunction<
Int32 Function(Pointer<Void>, Uint32, Uint32),
int Function(Pointer<Void>, int, int)>(
'SetHandleInformation',
);
final _CreateProcessW = _kernel32.lookupFunction<
Int32 Function(
Pointer<Utf16>,
Pointer<Utf16>,
Pointer<SECURITY_ATTRIBUTES>,
Pointer<SECURITY_ATTRIBUTES>,
Int32,
Uint32,
Pointer<Void>,
Pointer<Utf16>,
Pointer<STARTUPINFO>,
Pointer<PROCESS_INFORMATION>,
),
int Function(
Pointer<Utf16>,
Pointer<Utf16>,
Pointer<SECURITY_ATTRIBUTES>,
Pointer<SECURITY_ATTRIBUTES>,
int,
int,
Pointer<Void>,
Pointer<Utf16>,
Pointer<STARTUPINFO>,
Pointer<PROCESS_INFORMATION>,
)>(
'CreateProcessW',
);
final _PeekNamedPipe = _kernel32.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>),
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>)>(
'PeekNamedPipe',
);
final _ReadFile = _kernel32.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Void>),
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Void>)>(
'ReadFile',
);
final _CloseHandle = _kernel32.lookupFunction<
Int32 Function(Pointer<Void>),
int Function(Pointer<Void>)>(
'CloseHandle',
);
final _WaitForSingleObject = _kernel32.lookupFunction<
Uint32 Function(Pointer<Void>, Uint32),
int Function(Pointer<Void>, int)>(
'WaitForSingleObject',
);
final _GetStdHandle = _kernel32.lookupFunction<
IntPtr Function(Int32),
int Function(int)>(
'GetStdHandle',
);
final _GetExitCodeProcess = _kernel32.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Uint32>),
int Function(Pointer<Void>, Pointer<Uint32>)>(
'GetExitCodeProcess',
);
final _GetLastError = _kernel32.lookupFunction<
Uint32 Function(),
int Function()>(
'GetLastError',
);
// 🔧 WinINet API for proxy settings - reg
final DynamicLibrary _wininet = DynamicLibrary.open('wininet.dll');
const int INTERNET_OPTION_PROXY = 38;
const int INTERNET_OPEN_TYPE_PROXY = 3;
const int INTERNET_OPEN_TYPE_DIRECT = 1;
final class INTERNET_PROXY_INFO extends Struct {
@Int32()
external int dwAccessType;
external Pointer<Utf16> lpszProxy;
external Pointer<Utf16> lpszProxyBypass;
}
/// WinINet InternetSetOption API -
final _InternetSetOptionW = _wininet.lookupFunction<
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Uint32),
int Function(Pointer<Void>, int, Pointer<Void>, int)>(
'InternetSetOptionW',
);
/// WinINet InternetQueryOption API -
final _InternetQueryOptionW = _wininet.lookupFunction<
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Pointer<Uint32>),
int Function(Pointer<Void>, int, Pointer<Void>, Pointer<Uint32>)>(
'InternetQueryOptionW',
);
+93 -83
View File
@@ -106,7 +106,7 @@ class _HIDialogState extends State<HIDialog> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.r),
),
minimumSize: Size.fromHeight(40.w),
minimumSize: Size(85.w, 40.w),
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
@@ -120,7 +120,7 @@ class _HIDialogState extends State<HIDialog> {
),
)
: Text(
widget.confirmText ?? AppTranslations.kr_dialog.kr_confirm,
widget.confirmText ?? (widget.cancelText == null ? '好的' : AppTranslations.kr_dialog.kr_confirm),
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@@ -136,96 +136,106 @@ class _HIDialogState extends State<HIDialog> {
final dialog = Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(20.w),
child: ClipRRect(
borderRadius: BorderRadius.circular(34.r),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 26, sigmaY: 26), //
child: Container(
width: 245.w,
padding: EdgeInsets.fromLTRB(30.w, 16.w, 30.w, 16.w),
decoration: BoxDecoration(
color: const Color(0xF5F5F5).withOpacity(0.6), //
borderRadius: BorderRadius.circular(34.r),
border: Border.all(
color: Colors.white.withOpacity(0.2), //
width: 1.5,
child: Transform.translate(
offset: Offset(0, -30.w),
child: ClipRRect(
borderRadius: BorderRadius.circular(34.r),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 26, sigmaY: 26), //
child: Container(
width: 245.w,
padding: EdgeInsets.fromLTRB(30.w, 16.w, 30.w, 16.w),
decoration: BoxDecoration(
color: const Color(0xFFDEDEDE), //
borderRadius: BorderRadius.circular(34.r),
border: Border.all(
color: Colors.white.withOpacity(0.2), //
width: 1.5,
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.title != null) ...[
Align(
alignment: Alignment.centerLeft,
child: Text(
widget.title!,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
fontFamily: 'AlibabaPuHuiTi-Medium',
height: 1.3,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.title != null) ...[
Align(
alignment: Alignment.centerLeft,
child: Text(
widget.title!,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
height: 1.3,
),
),
),
),
SizedBox(height: 4.w),
],
if (widget.message != null || widget.customMessageWidget != null) ...[
Container(
constraints: BoxConstraints(maxHeight: 200.h),
child: SingleChildScrollView(
child: widget.customMessageWidget ??
Text(
widget.message!,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black,
height: 1.4,
fontFamily: 'AlibabaPuHuiTi-Medium',
),
),
),
),
],
if (widget.confirmText != null || widget.cancelText != null) ...[
SizedBox(height: 28.w),
Row(
children: [
if (widget.confirmText != null) Expanded(child: _buildConfirmButton()),
if (widget.cancelText != null && widget.confirmText != null)
SizedBox(width: 12.w),
if (widget.cancelText != null)
Expanded(
child: TextButton(
onPressed: () {
Get.back();
widget.onCancel?.call();
},
style: TextButton.styleFrom(
backgroundColor: const Color(0xFFFF00B7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.r),
),
minimumSize: Size.fromHeight(40.w),
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
widget.cancelText!,
SizedBox(height: 4.w),
],
if (widget.message != null || widget.customMessageWidget != null) ...[
Container(
constraints: BoxConstraints(maxHeight: 200.h),
child: SingleChildScrollView(
child: widget.customMessageWidget ??
Text(
widget.message!,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
fontSize: 14.sp,
color: Colors.black,
height: 1.4,
fontFamily: 'AlibabaPuHuiTi-Medium',
),
),
),
),
),
],
if (widget.confirmText != null || widget.cancelText != null || (widget.confirmText == null && widget.cancelText == null)) ...[
SizedBox(height: 12.w),
if (widget.confirmText == null && widget.cancelText == null)
Center(
child: SizedBox(
width: 85.w,
child: _buildConfirmButton(),
),
],
),
]
],
)
else
Row(
children: [
if (widget.confirmText != null) Expanded(child: _buildConfirmButton()),
if (widget.cancelText != null && widget.confirmText != null)
SizedBox(width: 12.w),
if (widget.cancelText != null)
Expanded(
child: TextButton(
onPressed: () {
Get.back();
widget.onCancel?.call();
},
style: TextButton.styleFrom(
backgroundColor: const Color(0xFFFF00B7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.r),
),
minimumSize: Size(85.w, 40.w),
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
widget.cancelText!,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
fontFamily: 'AlibabaPuHuiTi-Medium',
),
),
),
),
],
),
]
],
),
),
),
),
+7 -10
View File
@@ -6,7 +6,8 @@ import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'dart:math' as math; // math 使 pi
import 'package:flutter/gestures.dart';
import 'package:get/get.dart';
import '../routes/app_pages.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:url_launcher/url_launcher.dart';
///
class HICollapsibleItem {
@@ -61,15 +62,11 @@ class _HICollapsibleItemWidgetState extends State<HICollapsibleItemWidget> {
color: const Color(0xFFADFF5B), //
),
recognizer: TapGestureRecognizer()
..onTap = () {
//
// url_launcher
Get.toNamed(
Routes.KR_WEBVIEW,
arguments: {
'url': 'https://www.baidu.com',
},
);
..onTap = () async {
final Uri url = Uri.parse('https://hifastvpn.com/help');
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
Get.snackbar('错误', '无法打开链接: $url');
}
},
),
);
@@ -1,117 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
///
class KRLanguageSwitchDialog extends StatelessWidget {
const KRLanguageSwitchDialog({super.key});
///
static Future<void> kr_show() async {
final isChineseRegion = await KRLanguageUtils.checkInitialLanguage();
if (isChineseRegion) {
await Get.dialog(
const KRLanguageSwitchDialog(),
barrierDismissible: false,
);
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
width: 280.w,
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(24.r),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//
KrLocalImage(
imageName: 'language_switch',
width: 120.w,
height: 120.h,
),
SizedBox(height: 16.h),
//
Text(
'根据您所在地区以及您的语言设置是否切换到中文语言?',
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 24.h),
//
_kr_buildButton(
context: context,
text: '切换',
isPrimary: true,
onTap: () async {
final zhLanguage = KRLanguage.values.firstWhere(
(lang) => lang.countryCode == 'zh',
orElse: () => KRLanguage.zh,
);
await KRLanguageUtils.switchLanguage(zhLanguage);
Get.back();
},
),
SizedBox(height: 12.h),
//
_kr_buildButton(
context: context,
text: '不切换',
isPrimary: false,
onTap: () => Get.back(),
),
],
),
),
);
}
///
Widget _kr_buildButton({
required BuildContext context,
required String text,
required bool isPrimary,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Container(
width: double.infinity,
height: 44.h,
decoration: BoxDecoration(
color: isPrimary ? Colors.blue : Colors.transparent,
borderRadius: BorderRadius.circular(22.r),
border: isPrimary
? null
: Border.all(
color: Colors.blue,
width: 1,
),
),
alignment: Alignment.center,
child: Text(
text,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isPrimary ? Colors.white : Colors.blue,
),
),
),
);
}
}
@@ -42,9 +42,8 @@ class KRSubscriptionExpiryText extends StatelessWidget {
final day = expireDateTime.day.toString().padLeft(2, '0');
final hour = expireDateTime.hour.toString().padLeft(2, '0');
final minute = expireDateTime.minute.toString().padLeft(2, '0');
final second = expireDateTime.second.toString().padLeft(2, '0');
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
final formattedDateTime = '$year/$month/$day $hour:$minute';
expiryText = '到期时间:$formattedDateTime';
}
}
+32 -28
View File
@@ -110,41 +110,28 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
initialRoute: Routes.KR_SPLASH,
getPages: AppPages.routes,
builder: (context, child) {
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
///
ScreenUtil.init(context,
designSize: const Size(868, 668), minTextAdapt: true);
} else {
///
ScreenUtil.init(context,
designSize: const Size(375, 667), minTextAdapt: true);
}
/// - 使
ScreenUtil.init(
context,
designSize: const Size(375, 812),
minTextAdapt: true,
);
// child = FlutterEasyLoading(child: child); //
//
Widget wrappedChild = Listener(
onPointerDown: (_) async {
//
// try {
// final store = FMTCStore(KRFMTC.kr_storeName);
// if (!await store.manage.ready) {
// await store.manage.create();
// await KRFMTC.kr_initMapCache();
// }
// } catch (e) {
// print('地图缓存初始化失败: $e');
// }
},
child: child,
);
// Mac
if (Platform.isMacOS) {
//
if (Platform.isMacOS || Platform.isWindows) {
wrappedChild = MediaQuery(
data: MediaQuery.of(context).copyWith(
padding: MediaQuery.of(context).padding.copyWith(
top: 10.w, // Mac
top: 24.w, //
),
),
child: wrappedChild,
@@ -181,9 +168,15 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
transitionDuration: TransitionConfig.defaultDuration, //
customTransition: TransitionConfig.createDefaultTransition(),
routingCallback: (routing) {
if (routing == null) return;
if(Routes.KR_PURCHASE_MEMBERSHIP.contains(routing.current)) return;
//
if (routing == null || routing.current == null) return;
final String current = routing.current!;
print('🔥 routing.current: $current');
// UI /
GlobalOverlayService.instance.updateCurrentRoute(current);
//
const showButtonRoutes = [
Routes.MR_LOGIN,
Routes.HI_MENU,
@@ -191,11 +184,22 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
Routes.HI_USER_INFO,
Routes.KR_ORDER_STATUS,
];
print('routing.current${routing.current}');
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
if (showButtonRoutes.contains(routing.current)) {
// 使
bool isPurchasePage = current == Routes.KR_PURCHASE_MEMBERSHIP ||
current.contains(Routes.KR_PURCHASE_MEMBERSHIP) ||
Routes.KR_PURCHASE_MEMBERSHIP.contains(current);
if (isPurchasePage) {
//
GlobalOverlayService.instance.updateSubscriptionButtonColor(Colors.transparent);
GlobalOverlayService.instance.safeShowSubscriptionButton();
} else if (showButtonRoutes.any((r) => current == r || current.contains(r) || r.contains(current))) {
//
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
GlobalOverlayService.instance.safeShowSubscriptionButton();
} else {
//
GlobalOverlayService.instance.hideSubscriptionButton();
}
},