fix: 更新诸多bug
This commit is contained in:
@@ -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;
|
||||
})();
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
-294
@@ -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 为 null,Switch 自动禁用
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
-13
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 未初始化问题');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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时,第126行的验证才能正确检测到127.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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除备份数据
|
||||
///
|
||||
/// 在应用退出或不需要时调用
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user