fix: 更新诸多bug
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user