初始化提交
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/kr_home_controller.dart';
|
||||
|
||||
class KRHomeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<KRHomeController>(
|
||||
() => KRHomeController(),
|
||||
);
|
||||
Get.lazyPut<KRHomeController>(
|
||||
() => KRHomeController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1255
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
/// 首页基础视图状态枚举
|
||||
enum KRHomeViewsStatus {
|
||||
/// 未登录状态
|
||||
kr_notLoggedIn,
|
||||
|
||||
/// 已登录状态
|
||||
kr_loggedIn,
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// 首页列表视图状态枚举
|
||||
enum KRHomeViewsListStatus {
|
||||
kr_none,
|
||||
kr_loading,
|
||||
kr_error,
|
||||
kr_serverList,
|
||||
kr_countrySubscribeList,
|
||||
kr_serverSubscribeList,
|
||||
kr_subscribeList,
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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) {
|
||||
// 使用 GetX 的 .obs 变量来避免重复访问
|
||||
final hasValidSubscription =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value != null;
|
||||
final isTrial = controller.kr_subscribeService.kr_isTrial;
|
||||
final isLastDay = controller.kr_subscribeService.kr_isLastDayOfSubscription;
|
||||
final isNotLoggedIn = controller.kr_currentViewStatus.value ==
|
||||
KRHomeViewsStatus.kr_notLoggedIn;
|
||||
|
||||
KRLogUtil.kr_i('构建默认视图', tag: 'HomeBottomPanel');
|
||||
KRLogUtil.kr_i('是否未登录: $isNotLoggedIn', tag: 'HomeBottomPanel');
|
||||
KRLogUtil.kr_i('是否有有效订阅: $hasValidSubscription', tag: 'HomeBottomPanel');
|
||||
KRLogUtil.kr_i('是否试用: ${isTrial.value}', tag: 'HomeBottomPanel');
|
||||
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
|
||||
tag: 'HomeBottomPanel');
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 主要内容区域
|
||||
if (isNotLoggedIn)
|
||||
// 未登录状态下,使用 SingleChildScrollView 让内容自然撑开
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
|
||||
child: const KRHomeConnectionOptionsView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
// 已登录状态下,使用固定高度
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1. 如果已订阅,展示当前连接卡片
|
||||
if (hasValidSubscription)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 12.h),
|
||||
child: const KRHomeConnectionInfoView())
|
||||
else
|
||||
Container(
|
||||
margin:
|
||||
EdgeInsets.only(top: 12.h, left: 12.w, right: 12.w),
|
||||
child: const KRSubscriptionCard()),
|
||||
|
||||
// 2. 如果已订阅且是试用,展示试用卡片
|
||||
if (hasValidSubscription && isTrial.value)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 12.h),
|
||||
child: const KRHomeTrialCard(),
|
||||
),
|
||||
|
||||
// 3. 如果已订阅且是最后一天,展示最后一天卡片
|
||||
if (hasValidSubscription && isLastDay.value && !isTrial.value)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 12.h),
|
||||
child: const KRHomeLastDayCard(),
|
||||
),
|
||||
|
||||
// 4. 连接选项(分组和国家入口)
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
|
||||
child: const KRHomeConnectionOptionsView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _kr_buildLoadingView() {
|
||||
KRLogUtil.kr_i('构建加载视图', tag: 'HomeBottomPanel');
|
||||
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
|
||||
tag: 'HomeBottomPanel');
|
||||
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.green,
|
||||
strokeWidth: 2.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _kr_buildErrorView(BuildContext context) {
|
||||
return Container(
|
||||
height: 200.w,
|
||||
padding: EdgeInsets.all(16.w),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48.w,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Text(
|
||||
AppTranslations.kr_home.error,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Text(
|
||||
AppTranslations.kr_home.checkNetwork,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.w),
|
||||
SizedBox(
|
||||
width: 200.w,
|
||||
height: 44.h,
|
||||
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.r),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
AppTranslations.kr_home.retry,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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 '../controllers/kr_home_controller.dart';
|
||||
import '../models/kr_home_views_status.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.w),
|
||||
width: double.infinity,
|
||||
height: 116.w,
|
||||
decoration: ShapeDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(14.w),
|
||||
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.w,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
KRCountryFlag(
|
||||
countryCode: controller.kr_getCurrentNodeCountry(),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
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.w),
|
||||
Row(
|
||||
children: [
|
||||
Obx(() {
|
||||
final delay = controller.kr_currentNodeLatency.value;
|
||||
|
||||
// 获取延迟颜色
|
||||
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 '0ms';
|
||||
} else if (delay == -1) {
|
||||
return AppTranslations.kr_home.connecting;
|
||||
} else if (delay >= 3000) {
|
||||
return AppTranslations.kr_home.timeout;
|
||||
} else {
|
||||
return '${delay}ms';
|
||||
}
|
||||
}
|
||||
|
||||
if (delay == -1) {
|
||||
return Row(
|
||||
children: [
|
||||
KRSimpleLoading(
|
||||
color: Colors.green,
|
||||
size: 12.w,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
Text(
|
||||
'connecting',
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Icon(Icons.signal_cellular_alt,
|
||||
size: 12.w,
|
||||
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.w),
|
||||
Icon(Icons.arrow_upward,
|
||||
size: 12.w,
|
||||
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.w),
|
||||
Icon(Icons.arrow_downward,
|
||||
size: 12.w,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
CupertinoSwitch(
|
||||
value: controller.kr_isConnected.value,
|
||||
onChanged: (bool value) {
|
||||
controller.kr_toggleSwitch(value);
|
||||
},
|
||||
activeTrackColor: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.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) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
AppTranslations.kr_home.connectionSectionTitle,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildConnectionOption(
|
||||
"home_server",
|
||||
AppTranslations.kr_home.dedicatedServers,
|
||||
context,
|
||||
onTap: () {
|
||||
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_serverList);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Flexible(
|
||||
child: _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}) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (controller.kr_subscribeService.kr_currentSubscribe.value == null) {
|
||||
// 未订阅状态下跳转到购买会员页面
|
||||
Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP);
|
||||
} else {
|
||||
// 已订阅状态下执行原有的点击事件
|
||||
onTap?.call();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: icon,
|
||||
width: 32.w,
|
||||
height: 32.w,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12.w,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
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 '../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.w),
|
||||
width: double.infinity,
|
||||
decoration: ShapeDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(14.w),
|
||||
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: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
|
||||
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.w,
|
||||
color: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 倒计时显示
|
||||
SizedBox(height: 10.w),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer_outlined,
|
||||
color: Colors.blue,
|
||||
size: 16.w,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Text(
|
||||
AppTranslations.kr_home.lastDaySubscriptionMessage,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
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.w),
|
||||
Text(
|
||||
AppTranslations.kr_home.subscriptionEndMessage,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+912
@@ -0,0 +1,912 @@
|
||||
import 'dart:math';
|
||||
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 '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';
|
||||
|
||||
/// 节点列表视图组件
|
||||
/// 用于展示所有节点相关的列表视图
|
||||
class KRHomeNodeListView extends GetView<KRHomeController> {
|
||||
const KRHomeNodeListView({super.key});
|
||||
|
||||
// 添加常量定义
|
||||
static const Color krModernGreen = Color(0xFF4CAF50);
|
||||
static const Color krModernGreenLight = Color(0xFF81C784);
|
||||
|
||||
// 存储每个节点的随机延迟值(仅用于界面显示)
|
||||
static final Map<String, int> _fakeDelays = {};
|
||||
|
||||
/// 获取显示的延迟值
|
||||
int _getDisplayDelay(KRHomeController controller, KROutboundItem item) {
|
||||
// 如果已连接,使用真实的延迟值
|
||||
if (controller.kr_isConnected.value) {
|
||||
return item.urlTestDelay.value;
|
||||
}
|
||||
|
||||
// 如果未连接,使用随机延迟值
|
||||
if (!_fakeDelays.containsKey(item.tag)) {
|
||||
// 生成30ms-100ms之间的随机延迟
|
||||
final random = Random();
|
||||
_fakeDelays[item.tag] = 30 + random.nextInt(71); // 30 + (0-70) = 30-100ms
|
||||
}
|
||||
|
||||
return _fakeDelays[item.tag] ?? 0;
|
||||
}
|
||||
|
||||
@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: ScreenUtil().screenWidth,
|
||||
height: 360.w, // 减小高度比例
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20.w),
|
||||
topRight: Radius.circular(20.w),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 标题栏
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
|
||||
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.w,
|
||||
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.w),
|
||||
itemCount: controller.kr_subscribeService.groupOutboundList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final group =
|
||||
controller.kr_subscribeService.groupOutboundList[index];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 8.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
controller.kr_setCurrentGroup(group);
|
||||
controller.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_serverSubscribeList;
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12.w),
|
||||
child: Row(
|
||||
children: [
|
||||
KRNetworkImage(
|
||||
kr_imageUrl: group.icon,
|
||||
kr_width: 32.w,
|
||||
kr_height: 32.w,
|
||||
kr_fit: BoxFit.cover,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
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.w,
|
||||
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.w, 8.w, 16.w, 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.w),
|
||||
child: Row(
|
||||
children: [
|
||||
KRCountryFlag(
|
||||
countryCode: country.country,
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
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.w,
|
||||
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.w),
|
||||
itemCount: country.outboundList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final server = country.outboundList[index];
|
||||
return Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
print(server.tag);
|
||||
KRSingBoxImp.instance
|
||||
.kr_selectOutbound(server.tag);
|
||||
controller.kr_selectNode(server.tag);
|
||||
// 添加状态切换,回到默认状态
|
||||
controller.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.w,
|
||||
horizontal: 16.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
// 添加轻微的背景色以区分点击区域
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
child: _kr_buildNodeListItem(
|
||||
context,
|
||||
item: server,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 添加分隔线
|
||||
if (index < country.outboundList.length - 1)
|
||||
Divider(
|
||||
height: 1.w,
|
||||
indent: 16.w,
|
||||
endIndent: 16.w,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
Divider(
|
||||
height: 1.w,
|
||||
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.w, 16.w, 16.w, 0),
|
||||
itemCount: servers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final server = servers[index];
|
||||
return Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
KRLogUtil.kr_i(server.tag);
|
||||
KRSingBoxImp.instance.kr_selectOutbound(server.tag);
|
||||
controller.kr_selectNode(server.tag);
|
||||
// 添加状态切换,回到默认状态
|
||||
controller.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||
child: _kr_buildNodeListItem(
|
||||
context,
|
||||
item: server,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (index < servers.length - 1)
|
||||
Divider(
|
||||
height: 1.w,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _kr_buildListPage(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
VoidCallback? onBack,
|
||||
required Widget listContent,
|
||||
}) {
|
||||
return Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20.w),
|
||||
topRight: Radius.circular(20.w),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_kr_buildTitleBar(
|
||||
context,
|
||||
title: title,
|
||||
onBack: onBack,
|
||||
onClose: () =>
|
||||
controller.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none,
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: listContent),
|
||||
// 添加底部间距
|
||||
SizedBox(height: 12.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 抽取公共的标题栏组件
|
||||
Widget _kr_buildTitleBar(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
VoidCallback? onBack,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 16.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (onBack != null) ...[
|
||||
GestureDetector(
|
||||
onTap: onBack,
|
||||
child: Icon(
|
||||
Icons.arrow_back_ios,
|
||||
size: 20.w,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
],
|
||||
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.w,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 构建列表容器
|
||||
Widget _kr_buildListContainer(
|
||||
BuildContext context, {
|
||||
required Widget child,
|
||||
}) {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图标颜色
|
||||
Color? getIconColor(int delay) {
|
||||
if (delay == 0) {
|
||||
return null;
|
||||
} else if (delay >= 3000) {
|
||||
return Colors.red;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return Container(
|
||||
key: ValueKey(item.id),
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: "home_list_location",
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
color: getIconColor(item.urlTestDelay.value),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
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.w),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4.w, vertical: 1.w),
|
||||
decoration: BoxDecoration(
|
||||
color: krModernGreenLight.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
),
|
||||
child: Text(
|
||||
AppTranslations.kr_home.selected,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: krModernGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 2.w),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 自动触发延迟测试(仅在未连接状态下)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!controller.kr_isConnected.value && !controller.kr_isLatency.value) {
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'NodeListView');
|
||||
controller.kr_urlTest();
|
||||
}
|
||||
});
|
||||
return _kr_buildListContainer(
|
||||
context,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0),
|
||||
children: [
|
||||
// 延迟测试按钮作为第一个列表项
|
||||
InkWell(
|
||||
onTap: () => controller.kr_urlTest(),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
margin: EdgeInsets.only(top: 8.w), // 添加上方间距
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
decoration: BoxDecoration(
|
||||
color: krModernGreenLight.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: controller.kr_isLatency.value
|
||||
? KRSimpleLoading(
|
||||
color: krModernGreen,
|
||||
size: 24.w,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
)
|
||||
: Icon(
|
||||
Icons.speed,
|
||||
size: 24.w,
|
||||
color: krModernGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
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.w),
|
||||
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.w,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 分隔线
|
||||
Divider(
|
||||
height: 16.w,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
// Auto 选项
|
||||
InkWell(
|
||||
onTap: () {
|
||||
controller.kr_selectNode('auto');
|
||||
controller.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: "home_list_location",
|
||||
width: 36.w,
|
||||
height: 36.w,
|
||||
color: controller.kr_cutTag.value == 'auto'
|
||||
? Colors.green
|
||||
: null,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
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.w),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4.w, vertical: 1.w),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
krModernGreenLight.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
),
|
||||
child: Text(
|
||||
AppTranslations.kr_home.selected,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: krModernGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 2.w),
|
||||
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.w,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
// 节点列表
|
||||
...controller.kr_subscribeService.allList
|
||||
.map((node) => Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
KRLogUtil.kr_i(node.tag);
|
||||
KRSingBoxImp.instance.kr_selectOutbound(node.tag);
|
||||
controller.kr_selectNode(node.tag);
|
||||
controller.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||
child: _kr_buildNodeListItem(
|
||||
context,
|
||||
item: node,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (node !=
|
||||
controller.kr_subscribeService.allList.last)
|
||||
Divider(
|
||||
height: 1.w,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
))
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
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 '../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.w),
|
||||
width: double.infinity,
|
||||
decoration: ShapeDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(14.w),
|
||||
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: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
|
||||
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.w,
|
||||
color: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 倒计时显示
|
||||
SizedBox(height: 10.w),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer_outlined,
|
||||
color: Colors.blue,
|
||||
size: 16.w,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Text(
|
||||
AppTranslations.kr_home.trialing,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
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.w),
|
||||
Text(
|
||||
AppTranslations.kr_home.trialEndMessage,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
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_login/views/kr_login_view.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
import '../../../services/kr_subscribe_service.dart';
|
||||
import '../controllers/kr_home_controller.dart';
|
||||
import '../models/kr_home_views_status.dart';
|
||||
import '../widgets/kr_home_map_view.dart';
|
||||
import '../widgets/kr_subscribe_selector_view.dart';
|
||||
import 'kr_home_bottom_panel.dart';
|
||||
import 'kr_home_subscription_view.dart';
|
||||
|
||||
// 定义新的绿色
|
||||
const Color krModernGreen = Color(0xFF00E52B);
|
||||
const Color krModernGreenLight = Color(0xFF66FF85);
|
||||
const Color krModernGreenDark = Color(0xFF00B322);
|
||||
|
||||
class KRHomeView extends GetView<KRHomeController> {
|
||||
const KRHomeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (controller.kr_currentViewStatus.value ==
|
||||
KRHomeViewsStatus.kr_notLoggedIn) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// 地图视图
|
||||
const KRHomeMapView(),
|
||||
// 登录视图
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const KRLoginView(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
body: Stack(
|
||||
children: [
|
||||
// 地图视图
|
||||
const KRHomeMapView(),
|
||||
|
||||
// 顶部工具栏
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 左侧状态组
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context).cardColor
|
||||
: Theme.of(context).cardColor.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: krModernGreen,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Obx(() {
|
||||
return Text(
|
||||
controller.kr_connectText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 订阅视图
|
||||
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();
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (KRSubscribeService()
|
||||
.kr_currentStatus
|
||||
.value ==
|
||||
KRSubscribeServiceStatus.kr_loading) {
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: KRSubscribeSelectorView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin:
|
||||
const EdgeInsets.only(left: 12, right: 12),
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context).cardColor
|
||||
: Theme.of(context)
|
||||
.cardColor
|
||||
.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const KRHomeSubscriptionView(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 右侧按钮组
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 消息按钮
|
||||
Obx(() {
|
||||
if (!KRAppRunData().kr_isLogin.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context).cardColor
|
||||
: Theme.of(context).cardColor.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
Get.toNamed(Routes.KR_MESSAGE);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.notifications_outlined,
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Colors.blue
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color
|
||||
?.withOpacity(0.8),
|
||||
size: 16,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
// 刷新按钮
|
||||
Obx(() {
|
||||
if (!KRAppRunData().kr_isLogin.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context).cardColor
|
||||
: Theme.of(context).cardColor.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
controller.kr_refreshAll();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.refresh,
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Colors.blue
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.color
|
||||
?.withOpacity(0.8),
|
||||
size: 16,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 底部面板
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Obx(() {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
height: controller.kr_bottomPanelHeight.value.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const KRHomeBottomPanel(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Color _getTrafficColor(double percentage) {
|
||||
if (percentage >= 0.9) {
|
||||
return Colors.red;
|
||||
} else if (percentage >= 0.7) {
|
||||
return Colors.orange;
|
||||
} else {
|
||||
return krModernGreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../../../utils/kr_fm_tc.dart';
|
||||
import '../controllers/kr_home_controller.dart';
|
||||
import '../../../widgets/kr_local_image.dart';
|
||||
import '../../../utils/kr_log_util.dart';
|
||||
|
||||
/// 首页地图视图组件
|
||||
class KRHomeMapView extends GetView<KRHomeController> {
|
||||
const KRHomeMapView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 初始化地图缓存
|
||||
// KRFMTC.kr_initMapCache();
|
||||
|
||||
return Obx(() => FlutterMap(
|
||||
mapController: controller.kr_mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: _kr_getInitialMapCenter(),
|
||||
initialZoom: 4.0,
|
||||
initialRotation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
onMapEvent: (event) {
|
||||
try {
|
||||
if (event is MapEventMoveEnd) {
|
||||
if (event.source == MapEventSource.dragEnd ||
|
||||
event.source == MapEventSource.multiFingerEnd) {
|
||||
controller.kr_isUserMoving.value = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('地图事件处理失败: $e', tag: 'HomeMapView');
|
||||
}
|
||||
},
|
||||
keepAlive: true,
|
||||
interactionOptions: InteractionOptions(
|
||||
enableMultiFingerGestureRace: true,
|
||||
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
_kr_buildTileLayer(context),
|
||||
MarkerLayer(
|
||||
markers: controller.kr_subscribeService.allList
|
||||
.map((item) => _buildStyledMarker(item))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
/// 构建地图瓦片层
|
||||
Widget _kr_buildTileLayer(BuildContext context) {
|
||||
return TileLayer(
|
||||
urlTemplate: KRFMTC.kr_getTileUrl(),
|
||||
subdomains: KRFMTC.kr_getTileSubdomains(),
|
||||
userAgentPackageName: 'app.brAccelerator.com',
|
||||
tileProvider: KRFMTC.kr_getTileProvider(),
|
||||
tileBuilder: (context, child, tileImage) {
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建样式化的标记
|
||||
Marker _buildStyledMarker(dynamic node) {
|
||||
return Marker(
|
||||
point: LatLng(node.latitude, node.longitude),
|
||||
width: 42.w,
|
||||
height: 34.w,
|
||||
child: GetBuilder<KRHomeController>(
|
||||
id: node.tag,
|
||||
builder: (controller) {
|
||||
// 确定颜色
|
||||
Color? markerColor;
|
||||
if (node.urlTestDelay.value == 0) {
|
||||
// 延迟为0时使用默认颜色
|
||||
markerColor = null;
|
||||
} else if (node.urlTestDelay.value < 500) {
|
||||
// 延迟小于500ms显示绿色
|
||||
markerColor = const Color(0xFF00E52B).withOpacity(0.7);
|
||||
} else if (node.urlTestDelay.value < 3000) {
|
||||
// 延迟小于3000ms显示黄色
|
||||
markerColor = Colors.yellow;
|
||||
} else {
|
||||
// 超时显示红色
|
||||
markerColor = Colors.red;
|
||||
}
|
||||
|
||||
return KrLocalImage(
|
||||
imageName: "location",
|
||||
width: 42.w,
|
||||
height: 34.w,
|
||||
color: markerColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取初始地图中心点
|
||||
LatLng _kr_getInitialMapCenter() {
|
||||
if (controller.kr_isUserMoving.value) {
|
||||
return controller.kr_lastMapCenter.value;
|
||||
}
|
||||
|
||||
if (controller.kr_cutSeletedTag.isEmpty) {
|
||||
return const LatLng(35.0, 105.0); // 修改默认位置为中国中部
|
||||
}
|
||||
|
||||
final selectedNode = controller.kr_subscribeService.allList
|
||||
.firstWhereOrNull((item) => item.tag == controller.kr_cutSeletedTag.value);
|
||||
if (selectedNode == null) {
|
||||
return const LatLng(35.0, 105.0); // 修改默认位置为中国中部
|
||||
}
|
||||
|
||||
// 更新最后的地图中心点
|
||||
controller.kr_lastMapCenter.value =
|
||||
LatLng(selectedNode.latitude, selectedNode.longitude);
|
||||
return controller.kr_lastMapCenter.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
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.w),
|
||||
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.w, 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.w),
|
||||
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.w, 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.w),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.w),
|
||||
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.w),
|
||||
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.w),
|
||||
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.w),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
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/routes/app_pages.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) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 44.w,
|
||||
height: 44.w,
|
||||
margin: EdgeInsets.only(top: 16.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.language,
|
||||
color: Colors.blue,
|
||||
size: 26.w,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Text(
|
||||
AppTranslations.kr_home.subscriptionDescription,
|
||||
textAlign: TextAlign.center,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 42.h,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
AppTranslations.kr_home.subscribe,
|
||||
style: KrAppTextStyle(
|
||||
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.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
child: IntrinsicWidth(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user