初始化提交
This commit is contained in:
Executable
+95
@@ -0,0 +1,95 @@
|
||||
/// 接口名称
|
||||
abstract class Api {
|
||||
/// 游客登录查看是否已经注册
|
||||
static const String kr_isRegister = "/v1/app/auth/check";
|
||||
|
||||
/// 注册1024
|
||||
static const String kr_register = "/v1/app/auth/register";
|
||||
|
||||
/// 验证验证码
|
||||
static const String kr_checkVerificationCode =
|
||||
"/v1/common/check_verification_code";
|
||||
|
||||
/// 发送手机验证码
|
||||
static const String kr_sendPhoneCode = "/v1/common/send_sms_code";
|
||||
|
||||
/// 发送邮箱验证码
|
||||
static const String kr_sendEmailCode = "/v1/common/send_code";
|
||||
|
||||
/// 登录接口
|
||||
static const String kr_login = "/v1/app/auth/login";
|
||||
|
||||
/// 删除账号
|
||||
static const String kr_deleteAccount = "/v1/app/user/account";
|
||||
|
||||
/// 忘记密码-设置新密码
|
||||
static const String kr_setNewPsdByForgetPsd = "/v1/app/auth/reset_password";
|
||||
|
||||
/// 节点信息
|
||||
static const String kr_nodeList = "/v1/app/node/list";
|
||||
|
||||
/// 获取用户订阅流量日志
|
||||
static const String kr_nodeGroupList = "/v1/app/node/rule_group_list";
|
||||
|
||||
/// 预下单
|
||||
static const String kr_preOrder = "/v1/app/order/pre";
|
||||
|
||||
/// 获取下单zf方式
|
||||
static const String kr_getPaymentMethods = "/v1/app/payment/methods";
|
||||
|
||||
/// 进行下单
|
||||
static const String kr_purchase = "/v1/app/order/purchase";
|
||||
|
||||
/// 获取支付地址,跳转到付款地址
|
||||
static const String kr_checkout = "/v1/app/order/checkout";
|
||||
|
||||
/// 获取可购买套餐
|
||||
static const String kr_getPackageList = "/v1/app/subscribe/list";
|
||||
|
||||
/// 获取用户已订阅套餐
|
||||
static const String kr_getAlreadySubscribe =
|
||||
"/v1/app/subscribe/user/already_subscribe";
|
||||
|
||||
/// 获取用户可用订阅
|
||||
static const String kr_userAvailableSubscribe =
|
||||
"/v1/app/subscribe/user/available_subscribe";
|
||||
|
||||
/// 续费
|
||||
static const String kr_renewal = "/v1/app/order/renewal";
|
||||
|
||||
/// 获取用户订阅流量日志
|
||||
/// 通过该接口判断订单状态
|
||||
static const String kr_orderDetail = "/v1/app/order/detail";
|
||||
|
||||
/// 获取消息列表
|
||||
static const String kr_getMessageList = "/v1/app/announcement/list";
|
||||
|
||||
/// 获取邀请数据
|
||||
// static const String kr_getInviteData = "/v1/public/invite/code";
|
||||
|
||||
/// 配置信息
|
||||
static const String kr_config = "/v1/app/auth/config";
|
||||
|
||||
/// 获取用户信息
|
||||
static const String kr_getUserInfo = "/v1/app/user/info";
|
||||
|
||||
/// 获取用户在线时长统计
|
||||
static const String kr_getUserOnlineTimeStatistics =
|
||||
"/v1/app/user/online_time/statistics";
|
||||
|
||||
/// 获取用户邀请人数
|
||||
static const String kr_getAffiliateCount = "/v1/public/user/affiliate/count";
|
||||
|
||||
/// 获取站点协议
|
||||
static const String kr_getSiteTos = "/v1/common/site/tos";
|
||||
|
||||
/// 隐私政策
|
||||
static const String kr_getSitePrivacy = "/v1/common/site/privacy";
|
||||
|
||||
/// 获取网页文本内容
|
||||
static const String kr_getWebText = "/v1/common/site/text";
|
||||
|
||||
/// 重置订阅周期
|
||||
static const String kr_resetSubscribePeriod =
|
||||
"/v1/app/subscribe/reset/period";
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import '../../model/response/kr_config_data.dart';
|
||||
import '../../model/response/kr_kr_affiliate_count.dart';
|
||||
import '../../model/response/kr_message_list.dart';
|
||||
import '../../model/response/kr_user_info.dart';
|
||||
import '../../model/response/kr_user_online_duration.dart';
|
||||
import '../../model/response/kr_web_text.dart';
|
||||
import '../../network/base_response.dart';
|
||||
import '../../network/http_error.dart';
|
||||
import '../../network/http_util.dart';
|
||||
import 'api.dart';
|
||||
|
||||
class KRUserApi {
|
||||
// 创建一个单例实例
|
||||
static final KRUserApi _instance = KRUserApi._internal();
|
||||
factory KRUserApi() => _instance;
|
||||
|
||||
// 私有构造函数
|
||||
KRUserApi._internal();
|
||||
|
||||
/// 获取当前系统的 user_agent
|
||||
String _kr_getUserAgent() {
|
||||
if (Platform.isAndroid) {
|
||||
return 'android';
|
||||
} else if (Platform.isIOS) {
|
||||
return 'ios';
|
||||
} else if (Platform.isMacOS) {
|
||||
return 'mac';
|
||||
} else if (Platform.isWindows) {
|
||||
return 'windows';
|
||||
} else if (Platform.isLinux) {
|
||||
return 'linux';
|
||||
} else if (Platform.isFuchsia) {
|
||||
return 'harmony';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
Future<Either<HttpError, KRMessageList>> kr_getMessageList(
|
||||
int page, int size, {bool? popup}) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['page'] = page;
|
||||
data['size'] = size;
|
||||
if (popup != null) {
|
||||
data['popup'] = popup;
|
||||
}
|
||||
BaseResponse<dynamic> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRMessageList>(
|
||||
Api.kr_getMessageList,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
|
||||
Future<Either<HttpError, KRUserOnlineDurationResponse>> kr_getUserOnlineTimeStatistics(
|
||||
) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
BaseResponse<KRUserOnlineDurationResponse> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRUserOnlineDurationResponse>(
|
||||
Api.kr_getUserOnlineTimeStatistics,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
KRLogUtil.kr_i('获取用户在线时长统计: ${baseResponse.model}');
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
Future<Either<HttpError, KRUserInfo>> kr_getUserInfo() async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
BaseResponse<KRUserInfo> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRUserInfo>(
|
||||
Api.kr_getUserInfo,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
Future<Either<HttpError, KRAffiliateCount>> kr_getAffiliateCount() async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
BaseResponse<dynamic> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRAffiliateCount>(
|
||||
Api.kr_getAffiliateCount,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
Future<Either<HttpError, KRConfigData>> kr_config() async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['user_agent'] = _kr_getUserAgent();
|
||||
BaseResponse<KRConfigData> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRConfigData>(
|
||||
Api.kr_config,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/mixins/kr_app_bar_opacity_mixin.dart';
|
||||
import 'package:kaer_with_panels/app/services/api_service/api.dart';
|
||||
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
|
||||
import 'package:kaer_with_panels/app/model/response/kr_is_register.dart';
|
||||
import 'package:kaer_with_panels/app/model/response/kr_login_data.dart';
|
||||
import 'package:kaer_with_panels/app/network/base_response.dart';
|
||||
import 'package:kaer_with_panels/app/network/http_error.dart';
|
||||
import 'package:kaer_with_panels/app/network/http_util.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
|
||||
|
||||
import '../../utils/kr_common_util.dart';
|
||||
import '../../utils/kr_log_util.dart';
|
||||
|
||||
class KRAuthApi {
|
||||
/// 是否开启了审核开关
|
||||
Future<Either<HttpError, bool>> kr_isRegister(
|
||||
KRLoginType tpye, String account, String? areaCode) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['method'] = tpye.value;
|
||||
data['account'] = account;
|
||||
|
||||
final deviceId = await KRDeviceUtil().kr_getDeviceId();
|
||||
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
|
||||
data["identifier"] = deviceId;
|
||||
|
||||
data["user_agent"] = _kr_getUserAgent();
|
||||
data["os"] = _kr_getUserAgent();
|
||||
if (areaCode != null) {
|
||||
data['area_code'] = areaCode.toString();
|
||||
}
|
||||
|
||||
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
|
||||
.request<KRIsRegister>(Api.kr_isRegister, data,
|
||||
method: HttpMethod.POST, isShowLoading: true);
|
||||
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.kr_isRegister);
|
||||
}
|
||||
|
||||
/// 注册
|
||||
Future<Either<HttpError, String>> kr_register(
|
||||
KRLoginType tpye,
|
||||
String account,
|
||||
String? areaCode,
|
||||
String? code,
|
||||
String? password,
|
||||
{String? inviteCode}) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['method'] = tpye.value;
|
||||
data['account'] = account;
|
||||
data['password'] = password;
|
||||
data["code"] = code;
|
||||
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
|
||||
data["os"] = _kr_getUserAgent();
|
||||
|
||||
if (inviteCode != null && inviteCode.isNotEmpty) {
|
||||
data["invite"] = inviteCode;
|
||||
}
|
||||
data["user_agent"] = _kr_getUserAgent();
|
||||
if (tpye == KRLoginType.kr_telephone) {
|
||||
data['area_code'] = areaCode;
|
||||
}
|
||||
|
||||
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
|
||||
.request<KRLoginData>(Api.kr_register, data,
|
||||
method: HttpMethod.POST, isShowLoading: true);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.kr_token.toString());
|
||||
}
|
||||
|
||||
/// 验证验证码
|
||||
Future<Either<HttpError, bool>> kr_checkVerificationCode( KRLoginType tpye,
|
||||
String account, String? areaCode, String code, int type) async {
|
||||
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['method'] = tpye.value;
|
||||
if(tpye == KRLoginType.kr_telephone){
|
||||
data['account'] = areaCode.toString() + account;
|
||||
|
||||
}else{
|
||||
data['account'] = account;
|
||||
}
|
||||
data['code'] = code;
|
||||
data['type'] = type;
|
||||
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
|
||||
.request<KRIsRegister>(Api.kr_checkVerificationCode, data,
|
||||
method: HttpMethod.POST, isShowLoading: true);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
if(baseResponse.model.kr_isRegister){
|
||||
return right(true);
|
||||
}else{
|
||||
return left(HttpError(msg: "error.70001".tr, code: 70001));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// 登陆
|
||||
Future<Either<HttpError, String>> kr_login(KRLoginType tpye, bool isPsd,
|
||||
String account, String? areaCode, String? code, String? password) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['method'] = tpye.value;
|
||||
data['account'] = account;
|
||||
|
||||
|
||||
final deviceId = await KRDeviceUtil().kr_getDeviceId();
|
||||
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
|
||||
data["identifier"] = deviceId;
|
||||
data["user_agent"] = _kr_getUserAgent();
|
||||
data["os"] = _kr_getUserAgent();
|
||||
if (tpye == KRLoginType.kr_telephone) {
|
||||
data['area_code'] = areaCode;
|
||||
}
|
||||
|
||||
if (isPsd) {
|
||||
data['password'] = password;
|
||||
} else {
|
||||
data["code"] = code;
|
||||
}
|
||||
|
||||
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
|
||||
.request<KRLoginData>(Api.kr_login, data,
|
||||
method: HttpMethod.POST, isShowLoading: true);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.kr_token.toString());
|
||||
}
|
||||
|
||||
/// 发送验证码 type 1 注册 其他 2
|
||||
Future<Either<HttpError, bool>> kr_sendCode(
|
||||
KRLoginType tpye, String account, String? areaCode, int type) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
if (tpye == KRLoginType.kr_email) {
|
||||
data['email'] = account;
|
||||
} else {
|
||||
data['telephone'] = account;
|
||||
data['telephone_area_code'] = areaCode.toString();
|
||||
}
|
||||
data['type'] = type;
|
||||
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
|
||||
.request<dynamic>(
|
||||
tpye == KRLoginType.kr_email
|
||||
? Api.kr_sendEmailCode
|
||||
: Api.kr_sendPhoneCode,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
isShowLoading: true);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
// KRCommonUtil.kr_showToast(baseResponse.model.toString());
|
||||
// KRIsRegister model = (baseResponse..model) as KRIsRegister;
|
||||
return right(true);
|
||||
}
|
||||
|
||||
/// 删除账号
|
||||
Future<Either<HttpError, String>> kr_deleteAccount(KRLoginType tpye,
|
||||
String code) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['method'] = tpye.value;
|
||||
|
||||
data['code'] = code;
|
||||
|
||||
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
|
||||
.request<dynamic>(Api.kr_deleteAccount, data,
|
||||
method: HttpMethod.DELETE, isShowLoading: true);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right("");
|
||||
|
||||
}
|
||||
|
||||
/// 忘记密码-设置新密码
|
||||
Future<Either<HttpError, String>> kr_setNewPsdByForgetPsd(KRLoginType tpye,
|
||||
String account, String? areaCode, String? code, String? password) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['method'] = tpye.value;
|
||||
data['account'] = account;
|
||||
data['password'] = password;
|
||||
data["code"] = code;
|
||||
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
|
||||
data["user_agent"] = _kr_getUserAgent();
|
||||
data["os"] = _kr_getUserAgent();
|
||||
if (tpye == KRLoginType.kr_telephone) {
|
||||
data['area_code'] = areaCode;
|
||||
}
|
||||
|
||||
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
|
||||
.request<KRLoginData>(Api.kr_setNewPsdByForgetPsd, data,
|
||||
method: HttpMethod.POST, isShowLoading: true);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.kr_token.toString());
|
||||
}
|
||||
|
||||
String _kr_getUserAgent() {
|
||||
if (Platform.isAndroid) {
|
||||
return 'android';
|
||||
} else if (Platform.isIOS) {
|
||||
return 'ios';
|
||||
} else if (Platform.isMacOS) {
|
||||
return 'mac';
|
||||
} else if (Platform.isWindows) {
|
||||
return 'windows';
|
||||
} else if (Platform.isLinux) {
|
||||
return 'linux';
|
||||
} else if (Platform.isFuchsia) {
|
||||
return 'harmony';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_config.dart';
|
||||
import 'package:kaer_with_panels/app/services/api_service/api.dart';
|
||||
import 'package:kaer_with_panels/app/model/response/kr_login_data.dart';
|
||||
import 'package:kaer_with_panels/app/model/response/kr_node_list.dart';
|
||||
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
|
||||
import 'package:kaer_with_panels/app/network/base_response.dart';
|
||||
import 'package:kaer_with_panels/app/network/http_error.dart';
|
||||
import 'package:kaer_with_panels/app/network/http_util.dart';
|
||||
|
||||
import '../../model/response/kr_already_subscribe.dart';
|
||||
import '../../model/response/kr_node_group_list.dart';
|
||||
import '../../model/response/kr_order_status.dart';
|
||||
import '../../model/response/kr_payment_methods.dart';
|
||||
import '../../model/response/kr_purchase_order_no.dart';
|
||||
import '../../model/response/kr_status.dart';
|
||||
import '../../model/response/kr_user_available_subscribe.dart';
|
||||
|
||||
/// 订阅相关
|
||||
class KRSubscribeApi {
|
||||
/// 获取可购买套餐
|
||||
Future<Either<HttpError, KRPackageList>> kr_getPackageListList() async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
BaseResponse<KRPackageList> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRPackageList>(
|
||||
Api.kr_getPackageList,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
/// 获取节点列表
|
||||
Future<Either<HttpError, KRNodeList>> kr_nodeList(int id) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
BaseResponse<KRNodeList> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRNodeList>(
|
||||
Api.kr_nodeList,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
/// 获取用户可用订阅
|
||||
Future<Either<HttpError, List<KRUserAvailableSubscribeItem>>>
|
||||
kr_userAvailableSubscribe({bool containsNodes = false}) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['contains_nodes'] = containsNodes;
|
||||
|
||||
BaseResponse<KRUserAvailableSubscribeList> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRUserAvailableSubscribeList>(
|
||||
Api.kr_userAvailableSubscribe,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.list);
|
||||
}
|
||||
|
||||
/// 获取分组节点
|
||||
Future<Either<HttpError, List<KRNodeGroupListItem>>>
|
||||
kr_nodeGroupList() async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
BaseResponse<KRNodeGroupList> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRNodeGroupList>(
|
||||
Api.kr_nodeGroupList,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.list);
|
||||
}
|
||||
|
||||
/// 通过该接口判断订单状态
|
||||
Future<Either<HttpError, KROrderStatus>> kr_orderDetail(
|
||||
String orderId) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['order_no'] = orderId;
|
||||
|
||||
BaseResponse<KROrderStatus> baseResponse =
|
||||
await HttpUtil.getInstance().request<KROrderStatus>(
|
||||
Api.kr_orderDetail,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model);
|
||||
}
|
||||
|
||||
/// 获取支付方式
|
||||
Future<Either<HttpError, List<KRPaymentMethod>>>
|
||||
kr_getPaymentMethods() async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
BaseResponse<KRPaymentMethods> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRPaymentMethods>(
|
||||
Api.kr_getPaymentMethods,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.list);
|
||||
}
|
||||
|
||||
/// 进行下单
|
||||
Future<Either<HttpError, String>> kr_purchase(
|
||||
int planId, int quantity, int payment, String coupon) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['subscribe_id'] = planId;
|
||||
data['quantity'] = quantity;
|
||||
data['payment'] = payment;
|
||||
data['coupon'] = "";
|
||||
|
||||
BaseResponse<KRPurchaseOrderNo> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRPurchaseOrderNo>(
|
||||
Api.kr_purchase,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
isShowLoading: true,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.orderNo);
|
||||
}
|
||||
|
||||
/// 续费
|
||||
Future<Either<HttpError, String>> kr_renewal(
|
||||
int planId, int quantity, int payment, String coupon) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['user_subscribe_id'] = planId;
|
||||
data['quantity'] = quantity;
|
||||
data['payment'] = payment;
|
||||
data['coupon'] = "";
|
||||
|
||||
BaseResponse<KRPurchaseOrderNo> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRPurchaseOrderNo>(
|
||||
Api.kr_renewal,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
isShowLoading: true,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.orderNo);
|
||||
}
|
||||
|
||||
/// 获取用户已订阅套餐
|
||||
Future<Either<HttpError, List<KRAlreadySubscribe>>>
|
||||
kr_getAlreadySubscribe() async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
BaseResponse<KRAlreadySubscribeList> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRAlreadySubscribeList>(
|
||||
Api.kr_getAlreadySubscribe,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.list);
|
||||
}
|
||||
|
||||
Future<Either<HttpError, String>> kr_prePurchase(
|
||||
int planId, int quantity, String payment, String coupon) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['subscribe_id'] = planId;
|
||||
data['quantity'] = quantity;
|
||||
data['payment'] = payment;
|
||||
data['coupon'] = "";
|
||||
|
||||
BaseResponse<KRPurchaseOrderNo> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRPurchaseOrderNo>(
|
||||
Api.kr_preOrder,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
isShowLoading: true,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.orderNo);
|
||||
}
|
||||
|
||||
/// 获取支付地址,跳转到付款地址
|
||||
Future<Either<HttpError, String>> kr_checkout(String orderId) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['orderNo'] = orderId;
|
||||
data['returnUrl'] = AppConfig.getInstance().baseUrl;
|
||||
|
||||
BaseResponse<KRPurchaseOrderUrl> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRPurchaseOrderUrl>(
|
||||
Api.kr_checkout,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
isShowLoading: true,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.url);
|
||||
}
|
||||
|
||||
/// 重置订阅周期
|
||||
Future<Either<HttpError, bool>> kr_resetSubscribePeriod(
|
||||
int userSubscribeId) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['user_subscribe_id'] = userSubscribeId;
|
||||
BaseResponse<KRStatus> baseResponse =
|
||||
await HttpUtil.getInstance().request<KRStatus>(
|
||||
Api.kr_resetSubscribePeriod,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
isShowLoading: true,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(
|
||||
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
return right(baseResponse.model.kr_bl);
|
||||
}
|
||||
}
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:kaer_with_panels/app/services/api_service/api.dart';
|
||||
import 'package:kaer_with_panels/app/model/response/kr_web_text.dart';
|
||||
import 'package:kaer_with_panels/app/network/base_response.dart';
|
||||
import 'package:kaer_with_panels/app/network/http_error.dart';
|
||||
import 'package:kaer_with_panels/app/network/http_util.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
/// 网页相关 API
|
||||
class KRWebApi {
|
||||
/// 获取网页文本内容
|
||||
Future<Either<HttpError, String>> kr_getWebText(String url) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['url'] = url;
|
||||
|
||||
BaseResponse<KRWebText> baseResponse = await HttpUtil.getInstance().request<KRWebText>(
|
||||
url,
|
||||
data,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
|
||||
if (!baseResponse.isSuccess) {
|
||||
return left(HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||
}
|
||||
|
||||
// 根据 URL 返回对应的内容
|
||||
if (url.contains(Api.kr_getSitePrivacy)) {
|
||||
return right(baseResponse.model.privacyPolicy);
|
||||
} else if (url.contains(Api.kr_getSiteTos)) {
|
||||
return right(baseResponse.model.tosContent);
|
||||
} else {
|
||||
return right(baseResponse.model.privacyPolicy); // 默认返回隐私政策
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取网页内容
|
||||
// Future<Either<String, String>> kr_getWebContent() async {
|
||||
// try {
|
||||
// // ... 其他代码 ...
|
||||
// } catch (e) {
|
||||
// KRLogUtil.kr_e('获取网页内容失败: $e', tag: 'WebApi');
|
||||
// return Left('获取网页内容失败: $e');
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// 获取网页内容
|
||||
// Future<Either<String, String>> kr_getWebContentWithRetry() async {
|
||||
// try {
|
||||
// // ... 其他代码 ...
|
||||
// } catch (e) {
|
||||
// KRLogUtil.kr_e('获取网页内容失败: $e', tag: 'WebApi');
|
||||
// return Left('获取网页内容失败: $e');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../model/response/kr_message_list.dart';
|
||||
import 'api_service/kr_api.user.dart';
|
||||
import '../utils/kr_common_util.dart';
|
||||
import '../widgets/dialogs/kr_dialog.dart';
|
||||
import '../localization/app_translations.dart';
|
||||
|
||||
class KRAnnouncementService {
|
||||
static final KRAnnouncementService _instance = KRAnnouncementService._internal();
|
||||
final KRUserApi _kr_userApi = KRUserApi();
|
||||
bool _kr_hasShownAnnouncement = false;
|
||||
|
||||
factory KRAnnouncementService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
KRAnnouncementService._internal();
|
||||
|
||||
// 检查是否需要显示公告弹窗
|
||||
Future<void> kr_checkAnnouncement() async {
|
||||
if (_kr_hasShownAnnouncement) {
|
||||
return;
|
||||
}
|
||||
|
||||
final either = await _kr_userApi.kr_getMessageList(1, 1, popup: true);
|
||||
either.fold(
|
||||
(error) {
|
||||
KRCommonUtil.kr_showToast(error.msg);
|
||||
},
|
||||
(list) {
|
||||
if (list.announcements.isNotEmpty) {
|
||||
// 按创建时间降序排序,获取最新的公告
|
||||
final sortedAnnouncements = list.announcements;
|
||||
|
||||
final latestAnnouncement = sortedAnnouncements.first;
|
||||
|
||||
// 如果需要弹窗显示
|
||||
if (latestAnnouncement.popup) {
|
||||
_kr_hasShownAnnouncement = true;
|
||||
KRDialog.show(
|
||||
title: latestAnnouncement.title,
|
||||
message: null,
|
||||
confirmText: AppTranslations.kr_dialog.kr_iKnow,
|
||||
onConfirm: () {
|
||||
// Navigator.of(Get.context!).pop();
|
||||
},
|
||||
customMessageWidget: _kr_buildMessageContent(latestAnnouncement.content, Get.context!),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 构建消息内容
|
||||
Widget _kr_buildMessageContent(String content, BuildContext context) {
|
||||
// 判断内容类型
|
||||
final bool kr_isHtml = content.contains('<') && content.contains('>');
|
||||
final bool kr_isMarkdown = content.contains('**') ||
|
||||
content.contains('*') ||
|
||||
content.contains('#') ||
|
||||
content.contains('- ') ||
|
||||
content.contains('[');
|
||||
|
||||
final textStyle = TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
||||
height: 1.4,
|
||||
);
|
||||
|
||||
if (kr_isHtml) {
|
||||
// 使用 flutter_html 处理 HTML 内容
|
||||
return Html(
|
||||
data: content,
|
||||
style: {
|
||||
'body': Style(
|
||||
margin: Margins.all(0),
|
||||
padding: HtmlPaddings.all(0),
|
||||
fontSize: FontSize(14.sp),
|
||||
color: Theme.of(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 (kr_isMarkdown) {
|
||||
// 使用 flutter_markdown 处理 Markdown 内容
|
||||
return MarkdownBody(
|
||||
data: content,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
||||
height: 1.4,
|
||||
),
|
||||
strong: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
fontFamily: 'AlibabaPuHuiTi-Regular',
|
||||
height: 1.4,
|
||||
),
|
||||
em: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Theme.of(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 Text(
|
||||
content,
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+311
@@ -0,0 +1,311 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
/// WebSocket 服务类
|
||||
/// 用于处理与服务器的 WebSocket 连接、心跳和消息处理
|
||||
class KrSocketService {
|
||||
// 单例实例
|
||||
static final KrSocketService _instance = KrSocketService._internal();
|
||||
|
||||
// 私有变量
|
||||
WebSocket? _socket;
|
||||
StreamSubscription? _socketSubscription; // 添加订阅管理
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _heartbeatTimeoutTimer;
|
||||
int _heartbeatTimeoutCount = 0;
|
||||
Timer? _reconnectTimer;
|
||||
Timer? _vpnStateChangeTimer;
|
||||
String? _baseUrl;
|
||||
String? _userId;
|
||||
String? _deviceNumber;
|
||||
String? _token;
|
||||
|
||||
// 消息处理回调
|
||||
Function(Map<String, dynamic>)? _onMessageCallback;
|
||||
|
||||
// 连接状态回调
|
||||
Function(bool)? _onConnectionStateCallback;
|
||||
|
||||
|
||||
int _reconnectAttempts = 0;
|
||||
|
||||
// 连接状态
|
||||
bool _isConnecting = false;
|
||||
bool _isConnected = false;
|
||||
|
||||
// 连接状态检查
|
||||
bool _isConnectionStable = false;
|
||||
Timer? _connectionStabilityTimer;
|
||||
static const Duration _connectionStabilityTimeout = Duration(seconds: 10);
|
||||
|
||||
// 心跳相关
|
||||
static const int _maxHeartbeatTimeout = 3; // 最大心跳超时次数
|
||||
static const Duration _heartbeatTimeout = Duration(seconds: 10); // 心跳响应超时时间
|
||||
|
||||
// 私有构造函数
|
||||
KrSocketService._internal();
|
||||
|
||||
// 工厂构造函数
|
||||
factory KrSocketService() => _instance;
|
||||
|
||||
// 获取实例
|
||||
static KrSocketService get instance => _instance;
|
||||
|
||||
/// 初始化 WebSocket 服务
|
||||
void kr_init({
|
||||
required String baseUrl,
|
||||
required String userId,
|
||||
required String deviceNumber,
|
||||
required String token,
|
||||
}) {
|
||||
_baseUrl = baseUrl;
|
||||
_userId = userId;
|
||||
_deviceNumber = deviceNumber;
|
||||
_token = token;
|
||||
}
|
||||
|
||||
/// 设置消息处理回调
|
||||
void setOnMessageCallback(Function(Map<String, dynamic>) callback) {
|
||||
_onMessageCallback = callback;
|
||||
}
|
||||
|
||||
/// 设置连接状态回调
|
||||
void setOnConnectionStateCallback(Function(bool) callback) {
|
||||
_onConnectionStateCallback = callback;
|
||||
}
|
||||
|
||||
/// 连接到 WebSocket 服务器
|
||||
Future<void> connect() async {
|
||||
if (_isConnecting || _isConnected) {
|
||||
KRLogUtil.kr_i('WebSocket 正在连接或已连接,跳过重复连接', tag: 'WebSocket');
|
||||
return;
|
||||
}
|
||||
|
||||
_isConnecting = true;
|
||||
KRLogUtil.kr_i('开始连接 WebSocket...', tag: 'WebSocket');
|
||||
|
||||
try {
|
||||
// 确保 URL 使用 ws:// 或 wss:// 协议
|
||||
final uri = Uri.parse(_baseUrl!.startsWith('http')
|
||||
? _baseUrl!.replaceFirst('http', 'ws')
|
||||
: _baseUrl!);
|
||||
|
||||
// 构建 WebSocket URL,确保格式正确
|
||||
final wsUrl = Uri(
|
||||
scheme: uri.scheme,
|
||||
host: uri.host,
|
||||
port: uri.port,
|
||||
path: '/v1/app/ws/$_userId/$_deviceNumber',
|
||||
).toString();
|
||||
|
||||
KRLogUtil.kr_i('连接地址: $wsUrl', tag: 'WebSocket');
|
||||
|
||||
// 清理旧的连接
|
||||
_cleanup();
|
||||
|
||||
// 创建 WebSocket 连接
|
||||
_socket = await WebSocket.connect(
|
||||
wsUrl,
|
||||
headers: {
|
||||
'Authorization': _token!,
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade',
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
},
|
||||
);
|
||||
|
||||
// 设置消息监听并保存订阅
|
||||
_socketSubscription = _socket!.listen(
|
||||
(message) {
|
||||
KRLogUtil.kr_i('收到消息: $message', tag: 'WebSocket');
|
||||
_handleMessage(message);
|
||||
},
|
||||
onError: (error) {
|
||||
KRLogUtil.kr_e('WebSocket 错误: $error', tag: 'WebSocket');
|
||||
_handleConnectionError();
|
||||
},
|
||||
onDone: () {
|
||||
KRLogUtil.kr_i('WebSocket 连接关闭', tag: 'WebSocket');
|
||||
_handleConnectionError();
|
||||
},
|
||||
);
|
||||
|
||||
KRLogUtil.kr_i('WebSocket 连接成功', tag: 'WebSocket');
|
||||
_isConnected = true;
|
||||
_isConnecting = false;
|
||||
_reconnectAttempts = 0;
|
||||
|
||||
// 等待一小段时间后再发送心跳
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 开始心跳
|
||||
_startHeartbeat();
|
||||
|
||||
_onConnectionStateCallback?.call(true);
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('WebSocket 连接失败: $e', tag: 'WebSocket');
|
||||
KRLogUtil.kr_e('错误堆栈: $stackTrace', tag: 'WebSocket');
|
||||
_isConnecting = false;
|
||||
_handleConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 处理连接错误
|
||||
void _handleConnectionError() {
|
||||
_cleanup();
|
||||
|
||||
// 检查是否已登录
|
||||
if (!KRAppRunData.getInstance().kr_isLogin.value) {
|
||||
KRLogUtil.kr_i('用户已退出登录,停止重连', tag: 'WebSocket');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_reconnectAttempts++;
|
||||
|
||||
// 使用固定 5 秒的重连间隔
|
||||
const backoffDelay = Duration(seconds: 5);
|
||||
|
||||
KRLogUtil.kr_i('尝试重连 (第 $_reconnectAttempts 次, 间隔: ${backoffDelay.inSeconds}秒)...', tag: 'WebSocket');
|
||||
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = Timer(backoffDelay, () {
|
||||
connect();
|
||||
});
|
||||
}
|
||||
|
||||
/// 开始心跳
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimeoutTimer?.cancel();
|
||||
_heartbeatTimeoutCount = 0;
|
||||
|
||||
// 确保连接成功后再发送心跳
|
||||
if (_isConnected) {
|
||||
KRLogUtil.kr_i('发送初始心跳...', tag: 'WebSocket');
|
||||
sendMessage('ping');
|
||||
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 20), (timer) {
|
||||
if (_isConnected) {
|
||||
KRLogUtil.kr_i('发送心跳...', tag: 'WebSocket');
|
||||
sendMessage('ping');
|
||||
|
||||
// 启动心跳响应超时检测
|
||||
_heartbeatTimeoutTimer?.cancel();
|
||||
_heartbeatTimeoutTimer = Timer(_heartbeatTimeout, () {
|
||||
_heartbeatTimeoutCount++;
|
||||
KRLogUtil.kr_w('心跳响应超时 (第 $_heartbeatTimeoutCount 次)', tag: 'WebSocket');
|
||||
|
||||
if (_heartbeatTimeoutCount >= _maxHeartbeatTimeout) {
|
||||
KRLogUtil.kr_e('心跳响应连续超时 $_maxHeartbeatTimeout 次,主动断开重连', tag: 'WebSocket');
|
||||
_handleConnectionError();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
timer.cancel();
|
||||
_heartbeatTimeoutTimer?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理接收到的消息
|
||||
void _handleMessage(dynamic message) {
|
||||
try {
|
||||
if (message is String) {
|
||||
if (message == 'ping') {
|
||||
KRLogUtil.kr_i('收到心跳响应', tag: 'WebSocket');
|
||||
// 重置心跳超时计数
|
||||
_heartbeatTimeoutCount = 0;
|
||||
_heartbeatTimeoutTimer?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> data = json.decode(message);
|
||||
KRLogUtil.kr_i('处理消息: ${json.encode(data)}', tag: 'WebSocket');
|
||||
_onMessageCallback?.call(data);
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('消息处理错误: $e', tag: 'WebSocket');
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送消息
|
||||
void sendMessage(String message) {
|
||||
try {
|
||||
if (!_isConnected) {
|
||||
KRLogUtil.kr_w('WebSocket 未连接,无法发送消息', tag: 'WebSocket');
|
||||
return;
|
||||
}
|
||||
if (_socket == null) {
|
||||
KRLogUtil.kr_w('WebSocket 实例为空,无法发送消息', tag: 'WebSocket');
|
||||
return;
|
||||
}
|
||||
_socket!.add(message);
|
||||
KRLogUtil.kr_i('发送消息: $message', tag: 'WebSocket');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('发送消息失败: $e', tag: 'WebSocket');
|
||||
_handleConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送 JSON 消息
|
||||
void sendJsonMessage(Map<String, dynamic> message) {
|
||||
try {
|
||||
if (!_isConnected) {
|
||||
KRLogUtil.kr_w('WebSocket 未连接,无法发送消息', tag: 'WebSocket');
|
||||
return;
|
||||
}
|
||||
final jsonString = json.encode(message);
|
||||
sendMessage(jsonString);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('发送 JSON 消息失败: $e', tag: 'WebSocket');
|
||||
_handleConnectionError();
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理资源
|
||||
void _cleanup() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimeoutTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_vpnStateChangeTimer?.cancel();
|
||||
_connectionStabilityTimer?.cancel();
|
||||
|
||||
// 取消订阅
|
||||
_socketSubscription?.cancel();
|
||||
_socketSubscription = null;
|
||||
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_heartbeatTimer = null;
|
||||
_heartbeatTimeoutTimer = null;
|
||||
_reconnectTimer = null;
|
||||
_vpnStateChangeTimer = null;
|
||||
_connectionStabilityTimer = null;
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_isConnectionStable = false;
|
||||
_heartbeatTimeoutCount = 0;
|
||||
}
|
||||
|
||||
/// 关闭连接
|
||||
Future<void> disconnect() async {
|
||||
KRLogUtil.kr_i('关闭 WebSocket 连接', tag: 'WebSocket');
|
||||
_cleanup();
|
||||
_onConnectionStateCallback?.call(false);
|
||||
}
|
||||
|
||||
/// 检查连接状态
|
||||
bool get isConnected => _isConnected;
|
||||
}
|
||||
|
||||
|
||||
Executable
+551
@@ -0,0 +1,551 @@
|
||||
import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:kaer_with_panels/app/model/response/kr_node_group_list.dart';
|
||||
|
||||
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
|
||||
import 'package:kaer_with_panels/app/services/api_service/kr_subscribe_api.dart';
|
||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||
|
||||
import '../../singbox/model/singbox_status.dart';
|
||||
import '../model/business/kr_group_outbound_list.dart';
|
||||
import '../model/business/kr_outbound_item.dart';
|
||||
import '../model/business/kr_outbounds_list.dart';
|
||||
import '../model/response/kr_already_subscribe.dart';
|
||||
|
||||
/// 首页列表视图状态枚举
|
||||
enum KRSubscribeServiceStatus { kr_none, kr_loading, kr_error, kr_success }
|
||||
|
||||
/// 订阅服务类
|
||||
/// 用于管理用户订阅相关的所有操作
|
||||
class KRSubscribeService {
|
||||
/// 单例实例
|
||||
static final KRSubscribeService _instance = KRSubscribeService._internal();
|
||||
|
||||
/// 工厂构造函数
|
||||
factory KRSubscribeService() => _instance;
|
||||
|
||||
/// 私有构造函数
|
||||
KRSubscribeService._internal() {}
|
||||
|
||||
/// 订阅API
|
||||
final KRSubscribeApi kr_subscribeApi = KRSubscribeApi();
|
||||
|
||||
/// 可用订阅列表
|
||||
final RxList<KRUserAvailableSubscribeItem> kr_availableSubscribes =
|
||||
<KRUserAvailableSubscribeItem>[].obs;
|
||||
|
||||
/// 当前选中的订阅
|
||||
final Rx<KRUserAvailableSubscribeItem?> kr_currentSubscribe =
|
||||
Rx<KRUserAvailableSubscribeItem?>(null);
|
||||
|
||||
/// 节点分组列表
|
||||
final RxList<KRNodeGroupListItem> kr_nodeGroups = <KRNodeGroupListItem>[].obs;
|
||||
|
||||
/// 服务器分组
|
||||
final RxList<KRGroupOutboundList> groupOutboundList =
|
||||
<KRGroupOutboundList>[].obs;
|
||||
|
||||
/// 国家分组,包含所有国家
|
||||
final RxList<KRCountryOutboundList> countryOutboundList =
|
||||
<KRCountryOutboundList>[].obs;
|
||||
|
||||
/// 全部列表
|
||||
final RxList<KROutboundItem> allList = <KROutboundItem>[].obs;
|
||||
|
||||
/// 标签列表
|
||||
Map<String, KROutboundItem> keyList = {}; // 存储国家分组的列表
|
||||
|
||||
/// 试用剩余时间
|
||||
final RxString kr_trialRemainingTime = ''.obs;
|
||||
|
||||
/// 订阅剩余时间
|
||||
final RxString kr_subscriptionRemainingTime = ''.obs;
|
||||
|
||||
/// 剩余时间
|
||||
final RxString remainingTime = ''.obs;
|
||||
|
||||
/// 是否处于试用状态
|
||||
final RxBool kr_isTrial = false.obs;
|
||||
|
||||
/// 订阅记录
|
||||
final RxList<KRAlreadySubscribe> kr_alreadySubscribe =
|
||||
<KRAlreadySubscribe>[].obs;
|
||||
|
||||
/// 是否处于订阅最后一天
|
||||
final RxBool kr_isLastDayOfSubscription = false.obs;
|
||||
|
||||
/// 定期更新计时器
|
||||
Timer? _kr_updateTimer;
|
||||
|
||||
/// 试用倒计时计时器
|
||||
Timer? _kr_trialTimer;
|
||||
|
||||
/// 订阅倒计时计时器
|
||||
Timer? _kr_subscriptionTimer;
|
||||
|
||||
/// 当前状态
|
||||
final kr_currentStatus = KRSubscribeServiceStatus.kr_none.obs;
|
||||
|
||||
/// 重置订阅周期
|
||||
Future<void> kr_resetSubscribePeriod() async {
|
||||
if (kr_currentSubscribe.value == null) {
|
||||
KRCommonUtil.kr_showToast('请先选择订阅');
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await kr_subscribeApi
|
||||
.kr_resetSubscribePeriod(kr_currentSubscribe.value!.id);
|
||||
result.fold(
|
||||
(error) {
|
||||
KRCommonUtil.kr_showToast(error.msg);
|
||||
KRLogUtil.kr_e('重置订阅周期失败: ${error.msg}', tag: 'SubscribeService');
|
||||
},
|
||||
(success) {
|
||||
kr_refreshAll();
|
||||
KRLogUtil.kr_i('重置订阅周期成功', tag: 'SubscribeService');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取可用订阅列表
|
||||
Future<void> _kr_fetchAvailableSubscribes() async {
|
||||
try {
|
||||
KRLogUtil.kr_i('开始获取可用订阅列表', tag: 'SubscribeService');
|
||||
|
||||
final result = await kr_subscribeApi.kr_userAvailableSubscribe();
|
||||
|
||||
result.fold(
|
||||
(error) {
|
||||
KRLogUtil.kr_e('获取可用订阅失败: ${error.msg}', tag: 'SubscribeService');
|
||||
},
|
||||
(subscribes) {
|
||||
// 如果当前有选中的订阅,检查是否还在可用列表中
|
||||
if (kr_currentSubscribe.value != null) {
|
||||
final currentSubscribeExists = subscribes.any(
|
||||
(subscribe) => subscribe.id == kr_currentSubscribe.value?.id,
|
||||
);
|
||||
|
||||
// 如果当前订阅不在可用列表中,清除当前订阅
|
||||
if (!currentSubscribeExists) {
|
||||
// 如果当前订阅为null或者已过期,才设置新的订阅
|
||||
if (kr_currentSubscribe.value == null ||
|
||||
DateTime.parse(kr_currentSubscribe.value!.expireTime)
|
||||
.isBefore(DateTime.now())) {
|
||||
kr_availableSubscribes.assignAll(subscribes);
|
||||
if (subscribes.isNotEmpty) {
|
||||
kr_currentSubscribe.value = subscribes.first;
|
||||
KRLogUtil.kr_i('设置新的订阅: ${subscribes.first.name}',
|
||||
tag: 'SubscribeService');
|
||||
} else {
|
||||
kr_currentSubscribe.value = null;
|
||||
KRLogUtil.kr_i('没有可用的订阅,清除选中状态', tag: 'SubscribeService');
|
||||
}
|
||||
kr_clearCutNodeData();
|
||||
} else {
|
||||
KRLogUtil.kr_i('当前订阅仍然有效,保持选中状态', tag: 'SubscribeService');
|
||||
}
|
||||
} else {
|
||||
// 如果当前订阅仍然有效,更新为最新的订阅信息
|
||||
final updatedSubscribe = subscribes.firstWhere(
|
||||
(subscribe) => subscribe.id == kr_currentSubscribe.value?.id,
|
||||
);
|
||||
|
||||
// 检查订阅是否有效(未过期且未超出流量限制)
|
||||
final isExpired = DateTime.parse(updatedSubscribe.expireTime)
|
||||
.isBefore(DateTime.now());
|
||||
final isOverTraffic = updatedSubscribe.traffic > 0 &&
|
||||
(updatedSubscribe.download + updatedSubscribe.upload) >=
|
||||
updatedSubscribe.traffic;
|
||||
|
||||
if (isExpired || isOverTraffic) {
|
||||
if (KRSingBoxImp.instance.kr_status ==
|
||||
SingboxStatus.started()) {
|
||||
KRSingBoxImp.instance.kr_stop();
|
||||
}
|
||||
}
|
||||
|
||||
kr_currentSubscribe.value = updatedSubscribe;
|
||||
KRLogUtil.kr_i('更新当前订阅信息', tag: 'SubscribeService');
|
||||
|
||||
// 更新可用订阅列表
|
||||
kr_availableSubscribes.assignAll(subscribes);
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('获取可用订阅列表成功: ${subscribes.length} 个订阅',
|
||||
tag: 'SubscribeService');
|
||||
KRLogUtil.kr_i(
|
||||
'订阅列表: ${subscribes.map((s) => '${s.name}(${s.id})').join(', ')}',
|
||||
tag: 'SubscribeService');
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
KRLogUtil.kr_e('获取可用订阅异常: $err', tag: 'SubscribeService');
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换订阅
|
||||
Future<void> kr_switchSubscribe(
|
||||
KRUserAvailableSubscribeItem subscribe) async {
|
||||
// 如果切换的是当前订阅,直接返回
|
||||
if (subscribe.id == kr_currentSubscribe.value?.id) {
|
||||
KRLogUtil.kr_i('切换的订阅与当前订阅相同,无需切换', tag: 'SubscribeService');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_loading;
|
||||
await kr_clearCutNodeData();
|
||||
KRLogUtil.kr_i('开始切换订阅: ${subscribe.name + subscribe.id.toString()}',
|
||||
tag: 'SubscribeService');
|
||||
|
||||
// 更新当前订阅
|
||||
kr_currentSubscribe.value = subscribe;
|
||||
|
||||
final result =
|
||||
await kr_subscribeApi.kr_nodeList(kr_currentSubscribe.value!.id);
|
||||
|
||||
result.fold((error) {
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
|
||||
}, (nodes) {
|
||||
// 处理节点列表
|
||||
final listModel = KrOutboundsList();
|
||||
listModel.processOutboundItems(nodes.list, kr_nodeGroups);
|
||||
|
||||
// 更新UI数据
|
||||
groupOutboundList.value = listModel.groupOutboundList;
|
||||
countryOutboundList.value = listModel.countryOutboundList;
|
||||
allList.value = listModel.allList;
|
||||
keyList = listModel.keyList;
|
||||
|
||||
// 保存配置
|
||||
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
||||
|
||||
// 更新试用和订阅状态
|
||||
_kr_updateSubscribeStatus();
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_success;
|
||||
});
|
||||
} catch (e) {
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
|
||||
KRLogUtil.kr_e('切换订阅失败: $e', tag: 'SubscribeService');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新订阅状态
|
||||
void _kr_updateSubscribeStatus() {
|
||||
// 停止之前的计时器
|
||||
_kr_trialTimer?.cancel();
|
||||
_kr_subscriptionTimer?.cancel();
|
||||
|
||||
// 检查试用状态
|
||||
final bool kr_isSubscribed = kr_currentSubscribe.value != null &&
|
||||
kr_alreadySubscribe.any((subscribe) =>
|
||||
kr_currentSubscribe.value?.id == subscribe.userSubscribeId);
|
||||
|
||||
KRLogUtil.kr_i('当前订阅状态: ${kr_isSubscribed ? "已订阅" : "未订阅"}',
|
||||
tag: 'SubscribeService');
|
||||
KRLogUtil.kr_i('当前订阅ID: ${kr_currentSubscribe.value?.id}',
|
||||
tag: 'SubscribeService');
|
||||
KRLogUtil.kr_i(
|
||||
'已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}',
|
||||
tag: 'SubscribeService');
|
||||
|
||||
// 设置试用状态
|
||||
kr_isTrial.value = kr_currentSubscribe.value != null && !kr_isSubscribed;
|
||||
|
||||
KRLogUtil.kr_i('试用状态: ${kr_isTrial.value ? "是" : "否"}',
|
||||
tag: 'SubscribeService');
|
||||
|
||||
if (kr_isTrial.value) {
|
||||
// 启动试用倒计时
|
||||
_kr_startTrialTimer();
|
||||
}
|
||||
// 检查订阅状态
|
||||
else if (kr_currentSubscribe.value != null) {
|
||||
final expireTime = DateTime.parse(kr_currentSubscribe.value!.expireTime);
|
||||
final now = DateTime.now();
|
||||
final difference = expireTime.difference(now);
|
||||
|
||||
// 检查是否最后一天
|
||||
kr_isLastDayOfSubscription.value = difference.inDays <= 1;
|
||||
|
||||
if (kr_isLastDayOfSubscription.value) {
|
||||
// 启动订阅倒计时
|
||||
_kr_startSubscriptionTimer();
|
||||
KRLogUtil.kr_i('当前订阅最后一天', tag: 'SubscribeService');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动试用倒计时
|
||||
void _kr_startTrialTimer() {
|
||||
_kr_trialTimer?.cancel();
|
||||
|
||||
// 立即执行一次
|
||||
_kr_updateTrialTime();
|
||||
|
||||
// 设置定时器
|
||||
_kr_trialTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_kr_updateTrialTime();
|
||||
});
|
||||
}
|
||||
|
||||
/// 更新试用时间
|
||||
void _kr_updateTrialTime() {
|
||||
if (kr_currentSubscribe.value == null) {
|
||||
_kr_trialTimer?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final expireTime = DateTime.parse(kr_currentSubscribe.value!.expireTime);
|
||||
final now = DateTime.now();
|
||||
final difference = expireTime.difference(now);
|
||||
|
||||
if (difference.isNegative) {
|
||||
/// 停止
|
||||
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
|
||||
KRSingBoxImp.instance.kr_stop();
|
||||
}
|
||||
|
||||
_kr_trialTimer?.cancel();
|
||||
kr_trialRemainingTime.value = 'error.60001'.tr;
|
||||
return;
|
||||
}
|
||||
|
||||
final days = difference.inDays;
|
||||
final hours = difference.inHours % 24;
|
||||
final minutes = difference.inMinutes % 60;
|
||||
final seconds = difference.inSeconds % 60;
|
||||
|
||||
kr_trialRemainingTime.value = AppTranslations.kr_home.trialTimeWithDays(
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
);
|
||||
}
|
||||
|
||||
/// 启动订阅倒计时
|
||||
void _kr_startSubscriptionTimer() {
|
||||
_kr_subscriptionTimer?.cancel();
|
||||
|
||||
// 立即执行一次
|
||||
_kr_updateSubscriptionTime();
|
||||
|
||||
// 设置定时器
|
||||
_kr_subscriptionTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_kr_updateSubscriptionTime();
|
||||
});
|
||||
}
|
||||
|
||||
/// 更新订阅时间
|
||||
void _kr_updateSubscriptionTime() {
|
||||
if (kr_currentSubscribe.value == null) {
|
||||
_kr_subscriptionTimer?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final expireTime = DateTime.parse(kr_currentSubscribe.value!.expireTime);
|
||||
final now = DateTime.now();
|
||||
final difference = expireTime.difference(now);
|
||||
|
||||
if (difference.isNegative) {
|
||||
_kr_subscriptionTimer?.cancel();
|
||||
|
||||
/// 停止
|
||||
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
|
||||
KRSingBoxImp.instance.kr_stop();
|
||||
}
|
||||
|
||||
kr_subscriptionRemainingTime.value = 'error.60001'.tr;
|
||||
;
|
||||
return;
|
||||
}
|
||||
|
||||
final days = difference.inDays;
|
||||
final hours = difference.inHours % 24;
|
||||
final minutes = difference.inMinutes % 60;
|
||||
final seconds = difference.inSeconds % 60;
|
||||
|
||||
kr_subscriptionRemainingTime.value =
|
||||
AppTranslations.kr_home.trialTimeWithDays(
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
);
|
||||
}
|
||||
|
||||
/// 启动定期更新
|
||||
void _kr_startPeriodicUpdate() {
|
||||
// 每5分钟更新一次可用订阅列表
|
||||
_kr_updateTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
|
||||
_kr_fetchAvailableSubscribes();
|
||||
});
|
||||
}
|
||||
|
||||
/// 刷新所有数据
|
||||
Future<void> kr_refreshAll() async {
|
||||
try {
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_loading;
|
||||
await kr_clearData();
|
||||
KRLogUtil.kr_i('开始刷新所有数据', tag: 'SubscribeService');
|
||||
|
||||
/// 数组有值 ,表示订阅过, 用于判断试用的
|
||||
final alreadySubscribeResult =
|
||||
await kr_subscribeApi.kr_getAlreadySubscribe();
|
||||
alreadySubscribeResult.fold(
|
||||
(error) {
|
||||
throw Exception('获取已订阅列表失败: ${error.msg}');
|
||||
},
|
||||
(subscribes) {
|
||||
kr_alreadySubscribe.value = subscribes;
|
||||
KRLogUtil.kr_i('订阅记录: ${subscribes.length} 个订阅',
|
||||
tag: 'SubscribeService');
|
||||
},
|
||||
);
|
||||
|
||||
final result = await kr_subscribeApi.kr_nodeGroupList();
|
||||
result.fold(
|
||||
(error) {
|
||||
throw Exception('获取节点分组失败: ${error.msg}');
|
||||
},
|
||||
(groups) {
|
||||
kr_nodeGroups.value = groups;
|
||||
},
|
||||
);
|
||||
|
||||
// 保存当前选中的订阅名称
|
||||
final currentSubscribeID = kr_currentSubscribe.value?.id;
|
||||
|
||||
// 获取可用订阅列表
|
||||
final subscribeResult = await kr_subscribeApi.kr_userAvailableSubscribe();
|
||||
|
||||
// 处理订阅列表结果
|
||||
final subscribes = await subscribeResult.fold(
|
||||
(error) {
|
||||
throw Exception('获取可用订阅失败: ${error.msg}');
|
||||
},
|
||||
(subscribes) => subscribes,
|
||||
);
|
||||
|
||||
// 如果获取订阅列表失败,直接返回
|
||||
if (subscribes.isEmpty) {
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_none;
|
||||
kr_availableSubscribes.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新订阅列表
|
||||
kr_availableSubscribes.assignAll(subscribes);
|
||||
|
||||
KRLogUtil.kr_i('获取可用订阅列表成功: ${subscribes.length} 个订阅',
|
||||
tag: 'SubscribeService');
|
||||
|
||||
// 如果之前的订阅名称在可用列表中,保持选中
|
||||
if (subscribes.isNotEmpty) {
|
||||
final previousSubscribe = subscribes.firstWhere(
|
||||
(subscribe) => subscribe.id == currentSubscribeID,
|
||||
orElse: () => subscribes.first,
|
||||
);
|
||||
|
||||
if (previousSubscribe.id != currentSubscribeID) {
|
||||
kr_currentSubscribe.value = previousSubscribe;
|
||||
KRLogUtil.kr_i('切换订阅: ${previousSubscribe.name}',
|
||||
tag: 'SubscribeService');
|
||||
} else {
|
||||
kr_currentSubscribe.value = previousSubscribe;
|
||||
}
|
||||
|
||||
// 获取节点列表
|
||||
final nodeResult =
|
||||
await kr_subscribeApi.kr_nodeList(kr_currentSubscribe.value!.id);
|
||||
|
||||
// 处理节点列表结果
|
||||
final nodes = await nodeResult.fold(
|
||||
(error) {
|
||||
throw Exception('获取节点列表失败: ${error.msg}');
|
||||
},
|
||||
(nodes) => nodes,
|
||||
);
|
||||
|
||||
// 处理节点列表
|
||||
final listModel = KrOutboundsList();
|
||||
listModel.processOutboundItems(nodes.list, kr_nodeGroups);
|
||||
|
||||
// 更新UI数据
|
||||
groupOutboundList.value = listModel.groupOutboundList;
|
||||
countryOutboundList.value = listModel.countryOutboundList;
|
||||
allList.value = listModel.allList;
|
||||
keyList = listModel.keyList;
|
||||
|
||||
// 保存配置
|
||||
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
||||
// 更新试用和订阅状态
|
||||
_kr_updateSubscribeStatus();
|
||||
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_success;
|
||||
|
||||
_kr_startPeriodicUpdate();
|
||||
} else {
|
||||
KRLogUtil.kr_w('没有可用的订阅', tag: 'SubscribeService');
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
|
||||
return;
|
||||
}
|
||||
} catch (err, stackTrace) {
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
|
||||
KRLogUtil.kr_e('刷新数据异常: $err\n$stackTrace', tag: 'SubscribeService');
|
||||
}
|
||||
}
|
||||
|
||||
//// 清楚
|
||||
Future<void> kr_clearData() async {
|
||||
_kr_subscriptionTimer?.cancel();
|
||||
_kr_trialTimer?.cancel();
|
||||
_kr_updateTimer?.cancel();
|
||||
|
||||
kr_availableSubscribes.clear();
|
||||
|
||||
await kr_clearCutNodeData();
|
||||
}
|
||||
|
||||
Future<void> kr_logout() async {
|
||||
kr_alreadySubscribe.clear();
|
||||
kr_nodeGroups.clear();
|
||||
kr_currentSubscribe.value = null;
|
||||
|
||||
await kr_clearData();
|
||||
}
|
||||
|
||||
Future<void> kr_clearCutNodeData() async {
|
||||
kr_isLastDayOfSubscription.value = false;
|
||||
kr_isTrial.value = false;
|
||||
|
||||
kr_subscriptionRemainingTime.value = '';
|
||||
kr_trialRemainingTime.value = '';
|
||||
|
||||
/// 停止
|
||||
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
|
||||
await KRSingBoxImp.instance.kr_stop();
|
||||
}
|
||||
|
||||
// 更新UI数据
|
||||
groupOutboundList.clear();
|
||||
countryOutboundList.clear();
|
||||
allList.clear();
|
||||
keyList.clear();
|
||||
|
||||
// 保存配置
|
||||
KRSingBoxImp.instance.kr_saveOutbounds([]);
|
||||
}
|
||||
|
||||
/// 获取当前订阅
|
||||
KRUserAvailableSubscribeItem? get kr_getCurrentSubscribe =>
|
||||
kr_currentSubscribe.value;
|
||||
}
|
||||
+649
@@ -0,0 +1,649 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
|
||||
import 'package:kaer_with_panels/singbox/service/singbox_service_provider.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../../core/model/directories.dart';
|
||||
import '../../../singbox/model/singbox_config_option.dart';
|
||||
import '../../../singbox/model/singbox_outbound.dart';
|
||||
import '../../../singbox/model/singbox_stats.dart';
|
||||
import '../../../singbox/model/singbox_status.dart';
|
||||
import '../../utils/kr_country_util.dart';
|
||||
import '../../utils/kr_log_util.dart';
|
||||
|
||||
enum KRConnectionType {
|
||||
global,
|
||||
rule,
|
||||
// direct,
|
||||
}
|
||||
|
||||
class KRSingBoxImp {
|
||||
/// 私有构造函数
|
||||
KRSingBoxImp._();
|
||||
|
||||
/// 单例实例
|
||||
static final KRSingBoxImp _instance = KRSingBoxImp._();
|
||||
|
||||
/// 工厂构造函数
|
||||
factory KRSingBoxImp() => _instance;
|
||||
|
||||
/// 获取实例的静态方法
|
||||
static KRSingBoxImp get instance => _instance;
|
||||
|
||||
/// 配置文件目录
|
||||
late Directories kr_configDics;
|
||||
|
||||
/// 配置文件名称
|
||||
String kr_configName = "BearVPN";
|
||||
|
||||
/// 通道方法
|
||||
final _kr_methodChannel = const MethodChannel("com.baer.app/platform");
|
||||
|
||||
final _kr_container = ProviderContainer();
|
||||
|
||||
/// 核心服务
|
||||
late SingboxService kr_singBox;
|
||||
|
||||
/// more配置
|
||||
Map<String, dynamic> kr_configOption = {};
|
||||
|
||||
List<Map<String, dynamic>> kr_outbounds = [];
|
||||
|
||||
/// 首次启动
|
||||
RxBool kr_isFristStart = false.obs;
|
||||
|
||||
/// 状态
|
||||
final kr_status = SingboxStatus.stopped().obs;
|
||||
|
||||
/// 拦截广告
|
||||
final kr_blockAds = true.obs;
|
||||
|
||||
/// 是否自动自动选择线路
|
||||
final kr_isAutoOutbound = true.obs;
|
||||
|
||||
/// 连接类型
|
||||
final kr_connectionType = KRConnectionType.rule.obs;
|
||||
|
||||
String _cutPath = "";
|
||||
|
||||
/// 端口
|
||||
int kr_port = 51213;
|
||||
|
||||
/// 统计
|
||||
final kr_stats = SingboxStats(
|
||||
connectionsIn: 0,
|
||||
connectionsOut: 0,
|
||||
uplink: 0,
|
||||
downlink: 0,
|
||||
uplinkTotal: 0,
|
||||
downlinkTotal: 0,
|
||||
).obs;
|
||||
|
||||
/// 活动的出站分组
|
||||
RxList<SingboxOutboundGroup> kr_activeGroups = <SingboxOutboundGroup>[].obs;
|
||||
|
||||
/// 所有的出站分组
|
||||
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
|
||||
|
||||
/// Stream 订阅管理器
|
||||
final List<StreamSubscription<dynamic>> _kr_subscriptions = [];
|
||||
|
||||
/// 初始化
|
||||
Future<void> init() async {
|
||||
try {
|
||||
KRLogUtil.kr_i('开始初始化 SingBox');
|
||||
// 在应用启动时初始化
|
||||
await KRCountryUtil.kr_init();
|
||||
KRLogUtil.kr_i('国家工具初始化完成');
|
||||
|
||||
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
||||
KRLogUtil.kr_i('配置选项初始化完成');
|
||||
|
||||
KRLogUtil.kr_i('开始初始化 SingBox 服务');
|
||||
kr_singBox = await _kr_container.read(singboxServiceProvider);
|
||||
await _kr_container.read(singboxServiceProvider).init();
|
||||
KRLogUtil.kr_i('SingBox 服务初始化完成');
|
||||
|
||||
KRLogUtil.kr_i('开始初始化目录');
|
||||
|
||||
/// 初始化目录
|
||||
if (Platform.isIOS) {
|
||||
final paths = await _kr_methodChannel.invokeMethod<Map>("get_paths");
|
||||
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
|
||||
|
||||
kr_configDics = (
|
||||
baseDir: Directory(paths?["base"]! as String),
|
||||
workingDir: Directory(paths?["working"]! as String),
|
||||
tempDir: Directory(paths?["temp"]! as String),
|
||||
);
|
||||
} else {
|
||||
final baseDir = await getApplicationSupportDirectory();
|
||||
final workingDir =
|
||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
kr_configDics = (
|
||||
baseDir: baseDir,
|
||||
workingDir: workingDir!,
|
||||
tempDir: tempDir,
|
||||
);
|
||||
KRLogUtil.kr_i('其他平台路径初始化完成');
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('开始创建目录');
|
||||
if (!kr_configDics.baseDir.existsSync()) {
|
||||
await kr_configDics.baseDir.create(recursive: true);
|
||||
}
|
||||
if (!kr_configDics.workingDir.existsSync()) {
|
||||
await kr_configDics.workingDir.create(recursive: true);
|
||||
}
|
||||
if (!kr_configDics.tempDir.existsSync()) {
|
||||
await kr_configDics.tempDir.create(recursive: true);
|
||||
}
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
KRLogUtil.kr_i('目录创建完成');
|
||||
|
||||
KRLogUtil.kr_i('开始设置 SingBox');
|
||||
await kr_singBox.setup(kr_configDics, false).map((r) {
|
||||
KRLogUtil.kr_i('SingBox 设置成功');
|
||||
}).mapLeft((err) {
|
||||
KRLogUtil.kr_e('SingBox 设置失败: $err');
|
||||
throw err;
|
||||
}).run();
|
||||
|
||||
KRLogUtil.kr_i('开始更新 SingBox 选项');
|
||||
KRLogUtil.kr_i('📋 SingBox 配置选项: ${oOption.toJson()}', tag: 'SingBox');
|
||||
await kr_singBox.changeOptions(oOption)
|
||||
..map((r) {
|
||||
KRLogUtil.kr_i('✅ SingBox 选项更新成功', tag: 'SingBox');
|
||||
}).mapLeft((err) {
|
||||
KRLogUtil.kr_e('❌ SingBox 选项更新失败: $err', tag: 'SingBox');
|
||||
throw err;
|
||||
}).run();
|
||||
|
||||
KRLogUtil.kr_i('开始监听状态');
|
||||
// 初始订阅状态流
|
||||
kr_singBox.watchStatus().listen((status) {
|
||||
KRLogUtil.kr_i('🔄 SingBox 状态变化: $status', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 状态类型: ${status.runtimeType}', tag: 'SingBox');
|
||||
|
||||
// 确保状态更新
|
||||
kr_status.value = status;
|
||||
|
||||
switch (status) {
|
||||
case SingboxStopped():
|
||||
KRLogUtil.kr_i('🔴 SingBox 已停止', tag: 'SingBox');
|
||||
break;
|
||||
case SingboxStarting():
|
||||
KRLogUtil.kr_i('🟡 SingBox 正在启动', tag: 'SingBox');
|
||||
break;
|
||||
case SingboxStarted():
|
||||
KRLogUtil.kr_i('🟢 SingBox 已启动', tag: 'SingBox');
|
||||
kr_isFristStart.value = true;
|
||||
// 使用 GetX 的方式处理 Stream 订阅
|
||||
_kr_subscribeToStats();
|
||||
_kr_subscribeToGroups();
|
||||
// 强制触发状态更新
|
||||
kr_status.refresh();
|
||||
break;
|
||||
case SingboxStopping():
|
||||
KRLogUtil.kr_i('🟠 SingBox 正在停止', tag: 'SingBox');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('SingBox 初始化完成');
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('SingBox 初始化失败: $e');
|
||||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getConfigOption() {
|
||||
if (kr_configOption.isNotEmpty) {
|
||||
return kr_configOption;
|
||||
}
|
||||
final op = {
|
||||
"region": KRCountryUtil.kr_getCurrentCountryCode(),
|
||||
"block-ads": kr_blockAds.value,
|
||||
"use-xray-core-when-possible": false,
|
||||
"execute-config-as-is": false,
|
||||
"log-level": "warn",
|
||||
"resolve-destination": false,
|
||||
"ipv6-mode": "ipv4_only",
|
||||
// "remote-dns-address": "https://cloudflare-dns.com/dns-query",
|
||||
"remote-dns-address": "udp://1.1.1.1",
|
||||
"remote-dns-domain-strategy": "",
|
||||
"direct-dns-address": "223.5.5.5",
|
||||
"direct-dns-domain-strategy": "",
|
||||
"mixed-port": kr_port,
|
||||
"tproxy-port": kr_port,
|
||||
"local-dns-port": 36450,
|
||||
"tun-implementation": "gvisor",
|
||||
"mtu": 9000,
|
||||
"strict-route": true,
|
||||
// "connection-test-url": "http://www.cloudflare.com",
|
||||
"connection-test-url": "http://www.gstatic.com/generate_204",
|
||||
"url-test-interval": 30,
|
||||
"enable-clash-api": true,
|
||||
"clash-api-port": 36756,
|
||||
"enable-tun": Platform.isIOS || Platform.isAndroid,
|
||||
"enable-tun-service": false,
|
||||
"set-system-proxy":
|
||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
||||
"bypass-lan": false,
|
||||
"allow-connection-from-lan": false,
|
||||
"enable-fake-dns": false,
|
||||
"enable-dns-routing": true,
|
||||
"independent-dns-cache": true,
|
||||
"rules": [],
|
||||
"mux": {
|
||||
"enable": false,
|
||||
"padding": false,
|
||||
"max-streams": 8,
|
||||
"protocol": "h2mux"
|
||||
},
|
||||
"tls-tricks": {
|
||||
"enable-fragment": false,
|
||||
"fragment-size": "10-30",
|
||||
"fragment-sleep": "2-8",
|
||||
"mixed-sni-case": false,
|
||||
"enable-padding": false,
|
||||
"padding-size": "1-1500"
|
||||
},
|
||||
"warp": {
|
||||
"enable": false,
|
||||
"mode": "proxy_over_warp",
|
||||
"wireguard-config": "",
|
||||
"license-key": "",
|
||||
"account-id": "",
|
||||
"access-token": "",
|
||||
"clean-ip": "auto",
|
||||
"clean-port": 0,
|
||||
"noise": "1-3",
|
||||
"noise-size": "10-30",
|
||||
"noise-delay": "10-30",
|
||||
"noise-mode": "m4"
|
||||
},
|
||||
"warp2": {
|
||||
"enable": false,
|
||||
"mode": "proxy_over_warp",
|
||||
"wireguard-config": "",
|
||||
"license-key": "",
|
||||
"account-id": "",
|
||||
"access-token": "",
|
||||
"clean-ip": "auto",
|
||||
"clean-port": 0,
|
||||
"noise": "1-3",
|
||||
"noise-size": "10-30",
|
||||
"noise-delay": "10-30",
|
||||
"noise-mode": "m4"
|
||||
}
|
||||
};
|
||||
kr_configOption = op;
|
||||
return op;
|
||||
}
|
||||
|
||||
/// 订阅统计数据流
|
||||
void _kr_subscribeToStats() {
|
||||
// 取消之前的统计订阅
|
||||
for (var sub in _kr_subscriptions) {
|
||||
if (sub.hashCode.toString().contains('Stats')) {
|
||||
sub.cancel();
|
||||
}
|
||||
}
|
||||
_kr_subscriptions
|
||||
.removeWhere((sub) => sub.hashCode.toString().contains('Stats'));
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchStats().listen(
|
||||
(stats) {
|
||||
kr_stats.value = stats;
|
||||
},
|
||||
onError: (error) {
|
||||
KRLogUtil.kr_e('统计数据监听错误: $error');
|
||||
},
|
||||
cancelOnError: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 订阅分组数据流
|
||||
void _kr_subscribeToGroups() {
|
||||
// 取消之前的分组订阅
|
||||
for (var sub in _kr_subscriptions) {
|
||||
if (sub.hashCode.toString().contains('Groups')) {
|
||||
sub.cancel();
|
||||
}
|
||||
}
|
||||
_kr_subscriptions
|
||||
.removeWhere((sub) => sub.hashCode.toString().contains('Groups'));
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchActiveGroups().listen(
|
||||
(groups) {
|
||||
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
||||
kr_activeGroups.value = groups;
|
||||
|
||||
// 详细打印每个组的信息
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
final group = groups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox');
|
||||
},
|
||||
onError: (error) {
|
||||
KRLogUtil.kr_e('❌ 活动分组监听错误: $error', tag: 'SingBox');
|
||||
},
|
||||
cancelOnError: false,
|
||||
),
|
||||
);
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchGroups().listen(
|
||||
(groups) {
|
||||
kr_allGroups.value = groups;
|
||||
},
|
||||
onError: (error) {
|
||||
KRLogUtil.kr_e('所有分组监听错误: $error');
|
||||
},
|
||||
cancelOnError: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 监听活动组的详细实现
|
||||
// Future<void> watchActiveGroups() async {
|
||||
// try {
|
||||
// print("开始监听活动组详情...");
|
||||
|
||||
// final status = await kr_singBox.status();
|
||||
// print("服务状态: ${status.toJson()}");
|
||||
|
||||
// final outbounds = await kr_singBox.listOutbounds();
|
||||
// print("出站列表: ${outbounds.toJson()}");
|
||||
|
||||
// for (var outbound in outbounds.outbounds) {
|
||||
// print("出站配置: ${outbound.toJson()}");
|
||||
|
||||
// // 检查出站是否活动
|
||||
// final isActive = await kr_singBox.isOutboundActive(outbound.tag);
|
||||
// print("出站 ${outbound.tag} 活动状态: $isActive");
|
||||
// }
|
||||
// } catch (e, stack) {
|
||||
// print("监听活动组详情时出错: $e");
|
||||
// print("错误堆栈: $stack");
|
||||
// }
|
||||
// }
|
||||
|
||||
/// 保存配置文件
|
||||
void kr_saveOutbounds(List<Map<String, dynamic>> outbounds) async {
|
||||
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
|
||||
|
||||
kr_outbounds = outbounds;
|
||||
|
||||
final map = {};
|
||||
map["outbounds"] = kr_outbounds;
|
||||
|
||||
final file = _file(kr_configName);
|
||||
final temp = _tempFile(kr_configName);
|
||||
final mapStr = jsonEncode(map);
|
||||
|
||||
KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 配置文件前500字符: ${mapStr.substring(0, mapStr.length > 500 ? 500 : mapStr.length)}', tag: 'SingBox');
|
||||
|
||||
await file.writeAsString(mapStr);
|
||||
await temp.writeAsString(mapStr);
|
||||
|
||||
_cutPath = file.path;
|
||||
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
||||
|
||||
await kr_singBox
|
||||
.validateConfigByPath(file.path, temp.path, false)
|
||||
.mapLeft((err) {
|
||||
KRLogUtil.kr_e('❌ 保存配置文件失败: $err', tag: 'SingBox');
|
||||
}).run();
|
||||
|
||||
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
|
||||
}
|
||||
|
||||
Future<void> kr_start() async {
|
||||
kr_status.value = SingboxStarting();
|
||||
try {
|
||||
KRLogUtil.kr_i('🚀 开始启动 SingBox...', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
|
||||
|
||||
// 检查配置文件是否存在
|
||||
final configFile = File(_cutPath);
|
||||
if (await configFile.exists()) {
|
||||
final configContent = await configFile.readAsString();
|
||||
KRLogUtil.kr_i('📄 配置文件内容长度: ${configContent.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}', tag: 'SingBox');
|
||||
} else {
|
||||
KRLogUtil.kr_w('⚠️ 配置文件不存在: $_cutPath', tag: 'SingBox');
|
||||
}
|
||||
|
||||
await kr_singBox.start(_cutPath, kr_configName, false).map(
|
||||
(r) {
|
||||
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
|
||||
},
|
||||
).mapLeft((err) {
|
||||
KRLogUtil.kr_e('❌ SingBox 启动失败: $err', tag: 'SingBox');
|
||||
kr_status.value = SingboxStopped();
|
||||
throw err;
|
||||
}).run();
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox');
|
||||
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox');
|
||||
kr_status.value = SingboxStopped();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止服务
|
||||
Future<void> kr_stop() async {
|
||||
try {
|
||||
// 不主动赋值 kr_status
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await kr_singBox.stop().run();
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
// 取消订阅
|
||||
final subscriptions = List<StreamSubscription<dynamic>>.from(_kr_subscriptions);
|
||||
_kr_subscriptions.clear();
|
||||
for (var subscription in subscriptions) {
|
||||
try {
|
||||
await subscription.cancel();
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('取消订阅时出错: $e');
|
||||
}
|
||||
}
|
||||
// 不主动赋值 kr_status
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('停止服务时出错: $e');
|
||||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||||
// 兜底,防止状态卡死
|
||||
kr_status.value = SingboxStopped();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
void kr_updateAdBlockEnabled(bool bl) async {
|
||||
final oOption = _getConfigOption();
|
||||
|
||||
oOption["block-ads"] = bl;
|
||||
final op = SingboxConfigOption.fromJson(oOption);
|
||||
|
||||
await kr_singBox.changeOptions(op)
|
||||
..map((r) {}).mapLeft((err) {
|
||||
KRLogUtil.kr_e('更新广告拦截失败: $err');
|
||||
}).run();
|
||||
if (kr_status.value == SingboxStarted()) {
|
||||
await kr_restart();
|
||||
}
|
||||
kr_blockAds.value = bl;
|
||||
}
|
||||
|
||||
Future<void> kr_restart() async {
|
||||
KRLogUtil.kr_i("restart");
|
||||
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
|
||||
KRLogUtil.kr_e('重启失败: $err');
|
||||
}).run();
|
||||
}
|
||||
|
||||
//// 设置出站模式
|
||||
Future<void> kr_updateConnectionType(KRConnectionType newType) async {
|
||||
if (kr_connectionType.value == newType) {
|
||||
return;
|
||||
}
|
||||
|
||||
kr_connectionType.value = newType;
|
||||
|
||||
final oOption = _getConfigOption();
|
||||
|
||||
var mode = "";
|
||||
switch (newType) {
|
||||
case KRConnectionType.global:
|
||||
mode = "other";
|
||||
break;
|
||||
case KRConnectionType.rule:
|
||||
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||
break;
|
||||
// case KRConnectionType.direct:
|
||||
// mode = "direct";
|
||||
// break;
|
||||
}
|
||||
oOption["region"] = mode;
|
||||
final op = SingboxConfigOption.fromJson(oOption);
|
||||
|
||||
await kr_singBox.changeOptions(op)
|
||||
..map((r) {}).mapLeft((err) {
|
||||
KRLogUtil.kr_e('更新连接类型失败: $err');
|
||||
}).run();
|
||||
if (kr_status.value == SingboxStarted()) {
|
||||
await kr_restart();
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新国家设置
|
||||
Future<void> kr_updateCountry(KRCountry kr_country) async {
|
||||
// 如果国家相同,直接返回
|
||||
if (kr_country.kr_code == KRCountryUtil.kr_getCurrentCountryCode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新工具类中的当前国家
|
||||
await KRCountryUtil.kr_setCurrentCountry(kr_country);
|
||||
// 更新配置选项
|
||||
final oOption = _getConfigOption();
|
||||
oOption["region"] = kr_country.kr_code;
|
||||
final op = SingboxConfigOption.fromJson(oOption);
|
||||
|
||||
await kr_singBox.changeOptions(op)
|
||||
..map((r) {}).mapLeft((err) {
|
||||
KRLogUtil.kr_e('更新国家设置失败: $err');
|
||||
}).run();
|
||||
|
||||
// 如果服务正在运行,重启服务
|
||||
if (kr_status.value == SingboxStarted()) {
|
||||
await kr_restart();
|
||||
}
|
||||
} catch (err) {
|
||||
KRLogUtil.kr_e('更新国家失败: $err');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Stream<SingboxStatus> kr_watchStatus() {
|
||||
return kr_singBox.watchStatus();
|
||||
}
|
||||
|
||||
Stream<List<SingboxOutboundGroup>> kr_watchGroups() {
|
||||
return kr_singBox.watchGroups();
|
||||
}
|
||||
|
||||
void kr_selectOutbound(String tag) {
|
||||
KRLogUtil.kr_i('🎯 开始选择出站节点: $tag', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
|
||||
|
||||
// 打印所有活动组信息
|
||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||
final group = kr_activeGroups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
kr_singBox.selectOutbound("select", tag).run();
|
||||
}
|
||||
|
||||
/// 配合文件地址
|
||||
|
||||
Directory get directory =>
|
||||
Directory(p.join(kr_configDics.workingDir.path, "configs"));
|
||||
File _file(String fileName) {
|
||||
return File(p.join(directory.path, "$fileName.json"));
|
||||
}
|
||||
|
||||
File _tempFile(String fileName) => _file("$fileName.tmp");
|
||||
|
||||
// File tempFile(String fileName) => file("$fileName.tmp");
|
||||
|
||||
Future<void> kr_urlTest(String groupTag) async {
|
||||
KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
|
||||
|
||||
// 打印所有活动组信息
|
||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||
final group = kr_activeGroups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox');
|
||||
final result = await kr_singBox.urlTest(groupTag).run();
|
||||
KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox');
|
||||
|
||||
// 等待一段时间让 SingBox 完成测试
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
// 再次检查活动组状态
|
||||
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
|
||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||
final group = kr_activeGroups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ URL 测试失败: $groupTag, 错误: $e', tag: 'SingBox');
|
||||
KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user