初始化提交

This commit is contained in:
2025-09-23 16:23:15 +08:00
commit 877b18a70f
590 changed files with 53617 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
//
// Generated code. Do not modify.
// source: core.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class ParseConfigRequest extends $pb.GeneratedMessage {
factory ParseConfigRequest({
$core.String? tempPath,
$core.String? path,
$core.bool? debug,
}) {
final $result = create();
if (tempPath != null) {
$result.tempPath = tempPath;
}
if (path != null) {
$result.path = path;
}
if (debug != null) {
$result.debug = debug;
}
return $result;
}
ParseConfigRequest._() : super();
factory ParseConfigRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory ParseConfigRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ParseConfigRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'ConfigOptions'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'tempPath', protoName: 'tempPath')
..aOS(2, _omitFieldNames ? '' : 'path')
..aOB(3, _omitFieldNames ? '' : 'debug')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
ParseConfigRequest clone() => ParseConfigRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
ParseConfigRequest copyWith(void Function(ParseConfigRequest) updates) => super.copyWith((message) => updates(message as ParseConfigRequest)) as ParseConfigRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ParseConfigRequest create() => ParseConfigRequest._();
ParseConfigRequest createEmptyInstance() => create();
static $pb.PbList<ParseConfigRequest> createRepeated() => $pb.PbList<ParseConfigRequest>();
@$core.pragma('dart2js:noInline')
static ParseConfigRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ParseConfigRequest>(create);
static ParseConfigRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.String get tempPath => $_getSZ(0);
@$pb.TagNumber(1)
set tempPath($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasTempPath() => $_has(0);
@$pb.TagNumber(1)
void clearTempPath() => clearField(1);
@$pb.TagNumber(2)
$core.String get path => $_getSZ(1);
@$pb.TagNumber(2)
set path($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasPath() => $_has(1);
@$pb.TagNumber(2)
void clearPath() => clearField(2);
@$pb.TagNumber(3)
$core.bool get debug => $_getBF(2);
@$pb.TagNumber(3)
set debug($core.bool v) { $_setBool(2, v); }
@$pb.TagNumber(3)
$core.bool hasDebug() => $_has(2);
@$pb.TagNumber(3)
void clearDebug() => clearField(3);
}
class ParseConfigResponse extends $pb.GeneratedMessage {
factory ParseConfigResponse({
$core.String? error,
}) {
final $result = create();
if (error != null) {
$result.error = error;
}
return $result;
}
ParseConfigResponse._() : super();
factory ParseConfigResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory ParseConfigResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ParseConfigResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'ConfigOptions'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'error')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
ParseConfigResponse clone() => ParseConfigResponse()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
ParseConfigResponse copyWith(void Function(ParseConfigResponse) updates) => super.copyWith((message) => updates(message as ParseConfigResponse)) as ParseConfigResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ParseConfigResponse create() => ParseConfigResponse._();
ParseConfigResponse createEmptyInstance() => create();
static $pb.PbList<ParseConfigResponse> createRepeated() => $pb.PbList<ParseConfigResponse>();
@$core.pragma('dart2js:noInline')
static ParseConfigResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ParseConfigResponse>(create);
static ParseConfigResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.String get error => $_getSZ(0);
@$pb.TagNumber(1)
set error($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasError() => $_has(0);
@$pb.TagNumber(1)
void clearError() => clearField(1);
}
class GenerateConfigRequest extends $pb.GeneratedMessage {
factory GenerateConfigRequest({
$core.String? path,
$core.bool? debug,
}) {
final $result = create();
if (path != null) {
$result.path = path;
}
if (debug != null) {
$result.debug = debug;
}
return $result;
}
GenerateConfigRequest._() : super();
factory GenerateConfigRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory GenerateConfigRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GenerateConfigRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'ConfigOptions'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'path')
..aOB(2, _omitFieldNames ? '' : 'debug')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
GenerateConfigRequest clone() => GenerateConfigRequest()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
GenerateConfigRequest copyWith(void Function(GenerateConfigRequest) updates) => super.copyWith((message) => updates(message as GenerateConfigRequest)) as GenerateConfigRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GenerateConfigRequest create() => GenerateConfigRequest._();
GenerateConfigRequest createEmptyInstance() => create();
static $pb.PbList<GenerateConfigRequest> createRepeated() => $pb.PbList<GenerateConfigRequest>();
@$core.pragma('dart2js:noInline')
static GenerateConfigRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GenerateConfigRequest>(create);
static GenerateConfigRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.String get path => $_getSZ(0);
@$pb.TagNumber(1)
set path($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasPath() => $_has(0);
@$pb.TagNumber(1)
void clearPath() => clearField(1);
@$pb.TagNumber(2)
$core.bool get debug => $_getBF(1);
@$pb.TagNumber(2)
set debug($core.bool v) { $_setBool(1, v); }
@$pb.TagNumber(2)
$core.bool hasDebug() => $_has(1);
@$pb.TagNumber(2)
void clearDebug() => clearField(2);
}
class GenerateConfigResponse extends $pb.GeneratedMessage {
factory GenerateConfigResponse({
$core.String? config,
$core.String? error,
}) {
final $result = create();
if (config != null) {
$result.config = config;
}
if (error != null) {
$result.error = error;
}
return $result;
}
GenerateConfigResponse._() : super();
factory GenerateConfigResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory GenerateConfigResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GenerateConfigResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'ConfigOptions'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'config')
..aOS(2, _omitFieldNames ? '' : 'error')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
GenerateConfigResponse clone() => GenerateConfigResponse()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
GenerateConfigResponse copyWith(void Function(GenerateConfigResponse) updates) => super.copyWith((message) => updates(message as GenerateConfigResponse)) as GenerateConfigResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GenerateConfigResponse create() => GenerateConfigResponse._();
GenerateConfigResponse createEmptyInstance() => create();
static $pb.PbList<GenerateConfigResponse> createRepeated() => $pb.PbList<GenerateConfigResponse>();
@$core.pragma('dart2js:noInline')
static GenerateConfigResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GenerateConfigResponse>(create);
static GenerateConfigResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.String get config => $_getSZ(0);
@$pb.TagNumber(1)
set config($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasConfig() => $_has(0);
@$pb.TagNumber(1)
void clearConfig() => clearField(1);
@$pb.TagNumber(2)
$core.String get error => $_getSZ(1);
@$pb.TagNumber(2)
set error($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasError() => $_has(1);
@$pb.TagNumber(2)
void clearError() => clearField(2);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
+11
View File
@@ -0,0 +1,11 @@
//
// Generated code. Do not modify.
// source: core.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+79
View File
@@ -0,0 +1,79 @@
//
// Generated code. Do not modify.
// source: core.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:async' as $async;
import 'dart:core' as $core;
import 'package:grpc/service_api.dart' as $grpc;
import 'package:protobuf/protobuf.dart' as $pb;
import 'core.pb.dart' as $0;
export 'core.pb.dart';
@$pb.GrpcServiceName('ConfigOptions.CoreService')
class CoreServiceClient extends $grpc.Client {
static final _$parseConfig = $grpc.ClientMethod<$0.ParseConfigRequest, $0.ParseConfigResponse>(
'/ConfigOptions.CoreService/ParseConfig',
($0.ParseConfigRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.ParseConfigResponse.fromBuffer(value));
static final _$generateFullConfig = $grpc.ClientMethod<$0.GenerateConfigRequest, $0.GenerateConfigResponse>(
'/ConfigOptions.CoreService/GenerateFullConfig',
($0.GenerateConfigRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.GenerateConfigResponse.fromBuffer(value));
CoreServiceClient($grpc.ClientChannel channel,
{$grpc.CallOptions? options,
$core.Iterable<$grpc.ClientInterceptor>? interceptors})
: super(channel, options: options,
interceptors: interceptors);
$grpc.ResponseFuture<$0.ParseConfigResponse> parseConfig($0.ParseConfigRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$parseConfig, request, options: options);
}
$grpc.ResponseFuture<$0.GenerateConfigResponse> generateFullConfig($0.GenerateConfigRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$generateFullConfig, request, options: options);
}
}
@$pb.GrpcServiceName('ConfigOptions.CoreService')
abstract class CoreServiceBase extends $grpc.Service {
$core.String get $name => 'ConfigOptions.CoreService';
CoreServiceBase() {
$addMethod($grpc.ServiceMethod<$0.ParseConfigRequest, $0.ParseConfigResponse>(
'ParseConfig',
parseConfig_Pre,
false,
false,
($core.List<$core.int> value) => $0.ParseConfigRequest.fromBuffer(value),
($0.ParseConfigResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.GenerateConfigRequest, $0.GenerateConfigResponse>(
'GenerateFullConfig',
generateFullConfig_Pre,
false,
false,
($core.List<$core.int> value) => $0.GenerateConfigRequest.fromBuffer(value),
($0.GenerateConfigResponse value) => value.writeToBuffer()));
}
$async.Future<$0.ParseConfigResponse> parseConfig_Pre($grpc.ServiceCall call, $async.Future<$0.ParseConfigRequest> request) async {
return parseConfig(call, await request);
}
$async.Future<$0.GenerateConfigResponse> generateFullConfig_Pre($grpc.ServiceCall call, $async.Future<$0.GenerateConfigRequest> request) async {
return generateFullConfig(call, await request);
}
$async.Future<$0.ParseConfigResponse> parseConfig($grpc.ServiceCall call, $0.ParseConfigRequest request);
$async.Future<$0.GenerateConfigResponse> generateFullConfig($grpc.ServiceCall call, $0.GenerateConfigRequest request);
}
+77
View File
@@ -0,0 +1,77 @@
//
// Generated code. Do not modify.
// source: core.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use parseConfigRequestDescriptor instead')
const ParseConfigRequest$json = {
'1': 'ParseConfigRequest',
'2': [
{'1': 'tempPath', '3': 1, '4': 1, '5': 9, '10': 'tempPath'},
{'1': 'path', '3': 2, '4': 1, '5': 9, '10': 'path'},
{'1': 'debug', '3': 3, '4': 1, '5': 8, '10': 'debug'},
],
};
/// Descriptor for `ParseConfigRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List parseConfigRequestDescriptor = $convert.base64Decode(
'ChJQYXJzZUNvbmZpZ1JlcXVlc3QSGgoIdGVtcFBhdGgYASABKAlSCHRlbXBQYXRoEhIKBHBhdG'
'gYAiABKAlSBHBhdGgSFAoFZGVidWcYAyABKAhSBWRlYnVn');
@$core.Deprecated('Use parseConfigResponseDescriptor instead')
const ParseConfigResponse$json = {
'1': 'ParseConfigResponse',
'2': [
{'1': 'error', '3': 1, '4': 1, '5': 9, '9': 0, '10': 'error', '17': true},
],
'8': [
{'1': '_error'},
],
};
/// Descriptor for `ParseConfigResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List parseConfigResponseDescriptor = $convert.base64Decode(
'ChNQYXJzZUNvbmZpZ1Jlc3BvbnNlEhkKBWVycm9yGAEgASgJSABSBWVycm9yiAEBQggKBl9lcn'
'Jvcg==');
@$core.Deprecated('Use generateConfigRequestDescriptor instead')
const GenerateConfigRequest$json = {
'1': 'GenerateConfigRequest',
'2': [
{'1': 'path', '3': 1, '4': 1, '5': 9, '10': 'path'},
{'1': 'debug', '3': 2, '4': 1, '5': 8, '10': 'debug'},
],
};
/// Descriptor for `GenerateConfigRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List generateConfigRequestDescriptor = $convert.base64Decode(
'ChVHZW5lcmF0ZUNvbmZpZ1JlcXVlc3QSEgoEcGF0aBgBIAEoCVIEcGF0aBIUCgVkZWJ1ZxgCIA'
'EoCFIFZGVidWc=');
@$core.Deprecated('Use generateConfigResponseDescriptor instead')
const GenerateConfigResponse$json = {
'1': 'GenerateConfigResponse',
'2': [
{'1': 'config', '3': 1, '4': 1, '5': 9, '10': 'config'},
{'1': 'error', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'error', '17': true},
],
'8': [
{'1': '_error'},
],
};
/// Descriptor for `GenerateConfigResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List generateConfigResponseDescriptor = $convert.base64Decode(
'ChZHZW5lcmF0ZUNvbmZpZ1Jlc3BvbnNlEhYKBmNvbmZpZxgBIAEoCVIGY29uZmlnEhkKBWVycm'
'9yGAIgASgJSABSBWVycm9yiAEBQggKBl9lcnJvcg==');
+117
View File
@@ -0,0 +1,117 @@
import 'dart:io';
import 'package:freezed_annotation/freezed_annotation.dart';
// import 'package:kaer_with_panels/core/localization/translations.dart';
import 'package:kaer_with_panels/utils/platform_utils.dart';
@JsonEnum(valueField: 'key')
enum ServiceMode {
proxy("proxy"),
systemProxy("system-proxy"),
tun("vpn"),
tunService("vpn-service");
const ServiceMode(this.key);
final String key;
static ServiceMode get defaultMode =>
PlatformUtils.isDesktop ? systemProxy : tun;
/// supported service mode based on platform, use this instead of [values] in UI
static List<ServiceMode> get choices {
if (Platform.isWindows || Platform.isLinux) {
return values;
} else if (Platform.isMacOS) {
return [proxy, systemProxy, tun];
}
// mobile
return [proxy, tun];
}
bool get isExperimental => switch (this) {
tun => PlatformUtils.isDesktop,
tunService => PlatformUtils.isDesktop,
_ => false,
};
// String present(TranslationsEn t) => switch (this) {
// proxy => t.config.serviceModes.proxy,
// systemProxy => t.config.serviceModes.systemProxy,
// tun =>
// "${t.config.serviceModes.tun}${isExperimental ? " (${t.settings.experimental})" : ""}",
// tunService =>
// "${t.config.serviceModes.tunService}${isExperimental ? " (${t.settings.experimental})" : ""}",
// };
// String presentShort(TranslationsEn t) => switch (this) {
// proxy => t.config.shortServiceModes.proxy,
// systemProxy => t.config.shortServiceModes.systemProxy,
// tun => t.config.shortServiceModes.tun,
// tunService => t.config.shortServiceModes.tunService,
// };
}
@JsonEnum(valueField: 'key')
enum IPv6Mode {
disable("ipv4_only"),
enable("prefer_ipv4"),
prefer("prefer_ipv6"),
only("ipv6_only");
const IPv6Mode(this.key);
final String key;
// String present(TranslationsEn t) => switch (this) {
// disable => t.config.ipv6Modes.disable,
// enable => t.config.ipv6Modes.enable,
// prefer => t.config.ipv6Modes.prefer,
// only => t.config.ipv6Modes.only,
// };
}
@JsonEnum(valueField: 'key')
enum DomainStrategy {
auto(""),
preferIpv6("prefer_ipv6"),
preferIpv4("prefer_ipv4"),
ipv4Only("ipv4_only"),
ipv6Only("ipv6_only");
const DomainStrategy(this.key);
final String key;
String get displayName => switch (this) {
auto => "auto",
_ => key,
};
}
enum TunImplementation {
mixed,
system,
gvisor;
}
enum MuxProtocol {
h2mux,
smux,
yamux;
}
@JsonEnum(valueField: 'key')
enum WarpDetourMode {
proxyOverWarp("proxy_over_warp"),
warpOverProxy("warp_over_proxy");
const WarpDetourMode(this.key);
final String key;
// String present(TranslationsEn t) => switch (this) {
// proxyOverWarp => t.config.warpDetourModes.proxyOverWarp,
// warpOverProxy => t.config.warpDetourModes.warpOverProxy,
// };
}
+116
View File
@@ -0,0 +1,116 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:kaer_with_panels/core/model/optional_range.dart';
import 'package:kaer_with_panels/core/utils/json_converters.dart';
import 'package:kaer_with_panels/features/log/model/log_level.dart';
import 'package:kaer_with_panels/singbox/model/singbox_config_enum.dart';
import 'package:kaer_with_panels/singbox/model/singbox_rule.dart';
part 'singbox_config_option.freezed.dart';
part 'singbox_config_option.g.dart';
@freezed
class SingboxConfigOption with _$SingboxConfigOption {
const SingboxConfigOption._();
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxConfigOption({
required String region,
required bool blockAds,
required bool useXrayCoreWhenPossible,
required bool executeConfigAsIs,
required LogLevel logLevel,
required bool resolveDestination,
required IPv6Mode ipv6Mode,
required String remoteDnsAddress,
required DomainStrategy remoteDnsDomainStrategy,
required String directDnsAddress,
required DomainStrategy directDnsDomainStrategy,
required int mixedPort,
required int tproxyPort,
required int localDnsPort,
required TunImplementation tunImplementation,
required int mtu,
required bool strictRoute,
required String connectionTestUrl,
@IntervalInSecondsConverter() required Duration urlTestInterval,
required bool enableClashApi,
required int clashApiPort,
required bool enableTun,
required bool enableTunService,
required bool setSystemProxy,
required bool bypassLan,
required bool allowConnectionFromLan,
required bool enableFakeDns,
required bool enableDnsRouting,
required bool independentDnsCache,
// required String geoipPath,
// required String geositePath,
required List<SingboxRule> rules,
required SingboxMuxOption mux,
required SingboxTlsTricks tlsTricks,
required SingboxWarpOption warp,
required SingboxWarpOption warp2,
}) = _SingboxConfigOption;
String format() {
const encoder = JsonEncoder.withIndent(' ');
return encoder.convert(toJson());
}
factory SingboxConfigOption.fromJson(Map<String, dynamic> json) =>
_$SingboxConfigOptionFromJson(json);
}
@freezed
class SingboxWarpOption with _$SingboxWarpOption {
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxWarpOption({
required bool enable,
required WarpDetourMode mode,
required String wireguardConfig,
required String licenseKey,
required String accountId,
required String accessToken,
required String cleanIp,
required int cleanPort,
@OptionalRangeJsonConverter() required OptionalRange noise,
@OptionalRangeJsonConverter() required OptionalRange noiseSize,
@OptionalRangeJsonConverter() required OptionalRange noiseDelay,
@OptionalRangeJsonConverter() required String noiseMode,
}) = _SingboxWarpOption;
factory SingboxWarpOption.fromJson(Map<String, dynamic> json) =>
_$SingboxWarpOptionFromJson(json);
}
@freezed
class SingboxMuxOption with _$SingboxMuxOption {
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxMuxOption({
required bool enable,
required bool padding,
required int maxStreams,
required MuxProtocol protocol,
}) = _SingboxMuxOption;
factory SingboxMuxOption.fromJson(Map<String, dynamic> json) =>
_$SingboxMuxOptionFromJson(json);
}
@freezed
class SingboxTlsTricks with _$SingboxTlsTricks {
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxTlsTricks({
required bool enableFragment,
@OptionalRangeJsonConverter() required OptionalRange fragmentSize,
@OptionalRangeJsonConverter() required OptionalRange fragmentSleep,
required bool mixedSniCase,
required bool enablePadding,
@OptionalRangeJsonConverter() required OptionalRange paddingSize,
}) = _SingboxTlsTricks;
factory SingboxTlsTricks.fromJson(Map<String, dynamic> json) =>
_$SingboxTlsTricksFromJson(json);
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:dartx/dartx.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:kaer_with_panels/singbox/model/singbox_proxy_type.dart';
part 'singbox_outbound.freezed.dart';
part 'singbox_outbound.g.dart';
@freezed
class SingboxOutboundGroup with _$SingboxOutboundGroup {
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxOutboundGroup({
required String tag,
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
required String selected,
@Default([]) List<SingboxOutboundGroupItem> items,
}) = _SingboxOutboundGroup;
factory SingboxOutboundGroup.fromJson(Map<String, dynamic> json) =>
_$SingboxOutboundGroupFromJson(json);
}
@freezed
class SingboxOutboundGroupItem with _$SingboxOutboundGroupItem {
const SingboxOutboundGroupItem._();
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxOutboundGroupItem({
required String tag,
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
required int urlTestDelay,
}) = _SingboxOutboundGroupItem;
factory SingboxOutboundGroupItem.fromJson(Map<String, dynamic> json) =>
_$SingboxOutboundGroupItemFromJson(json);
}
final Map<String, ProxyType> _keyMap =
Map.fromEntries(ProxyType.values.map((e) => MapEntry(e.key, e)));
ProxyType _typeFromJson(dynamic type) => ProxyType.fromJson(type);
+45
View File
@@ -0,0 +1,45 @@
enum ProxyType {
direct("Direct"),
block("Block"),
dns("DNS"),
socks("SOCKS"),
http("HTTP"),
shadowsocks("Shadowsocks"),
vmess("VMess"),
trojan("Trojan"),
naive("Naive"),
wireguard("WireGuard"),
hysteria("Hysteria"),
tor("Tor"),
ssh("SSH"),
shadowtls("ShadowTLS"),
shadowsocksr("ShadowsocksR"),
vless("VLESS"),
tuic("TUIC"),
hysteria2("Hysteria2"),
selector("Selector"),
urltest("URLTest"),
warp("Warp"),
xvless("xVLESS"),
xvmess("xVMess"),
xtrojan("xTrojan"),
xfreedom("xFragment"),
xshadowsocks("xShadowsocks"),
xsocks("xSocks"),
invalid("Invalid"),
unknown("Unknown");
const ProxyType(this.label);
final String label;
String get key => name;
static List<ProxyType> groupValues = [selector, urltest];
bool get isGroup => ProxyType.groupValues.contains(this);
static final Map<String, ProxyType> _keyMap = Map.fromEntries(ProxyType.values.map((e) => MapEntry(e.key, e)));
static ProxyType fromJson(dynamic type) => _keyMap[(type as String?)?.toLowerCase()] ?? ProxyType.unknown;
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'singbox_rule.freezed.dart';
part 'singbox_rule.g.dart';
@freezed
class SingboxRule with _$SingboxRule {
const SingboxRule._();
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxRule({
String? ruleSetUrl,
String? domains,
String? ip,
String? port,
String? protocol,
@Default(RuleNetwork.tcpAndUdp) RuleNetwork network,
@Default(RuleOutbound.proxy) RuleOutbound outbound,
}) = _SingboxRule;
factory SingboxRule.fromJson(Map<String, dynamic> json) => _$SingboxRuleFromJson(json);
}
enum RuleOutbound { proxy, bypass, block }
@JsonEnum(valueField: 'key')
enum RuleNetwork {
tcpAndUdp(""),
tcp("tcp"),
udp("udp");
const RuleNetwork(this.key);
final String? key;
}
+22
View File
@@ -0,0 +1,22 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'singbox_stats.freezed.dart';
part 'singbox_stats.g.dart';
@freezed
class SingboxStats with _$SingboxStats {
const SingboxStats._();
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxStats({
required int connectionsIn,
required int connectionsOut,
required int uplink,
required int downlink,
required int uplinkTotal,
required int downlinkTotal,
}) = _SingboxStats;
factory SingboxStats.fromJson(Map<String, dynamic> json) =>
_$SingboxStatsFromJson(json);
}
+50
View File
@@ -0,0 +1,50 @@
import 'package:dartx/dartx.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'singbox_status.freezed.dart';
@freezed
sealed class SingboxStatus with _$SingboxStatus {
const SingboxStatus._();
const factory SingboxStatus.stopped({
SingboxAlert? alert,
String? message,
}) = SingboxStopped;
const factory SingboxStatus.starting() = SingboxStarting;
const factory SingboxStatus.started() = SingboxStarted;
const factory SingboxStatus.stopping() = SingboxStopping;
factory SingboxStatus.fromEvent(dynamic event) {
switch (event) {
case {
"status": "Stopped",
"alert": final String? alertStr,
"message": final String? messageStr,
}:
final alert = SingboxAlert.values.firstOrNullWhere(
(e) => alertStr?.toLowerCase() == e.name.toLowerCase(),
);
return SingboxStatus.stopped(alert: alert, message: messageStr);
case {"status": "Stopped"}:
return const SingboxStatus.stopped();
case {"status": "Starting"}:
return const SingboxStarting();
case {"status": "Started"}:
return const SingboxStarted();
case {"status": "Stopping"}:
return const SingboxStopping();
default:
throw Exception("unexpected status [$event]");
}
}
}
enum SingboxAlert {
requestVPNPermission,
requestNotificationPermission,
emptyConfiguration,
startCommandServer,
createService,
startService;
}
+26
View File
@@ -0,0 +1,26 @@
import 'dart:convert';
typedef WarpResponse = ({
String log,
String accountId,
String accessToken,
String wireguardConfig,
});
WarpResponse warpFromJson(dynamic json) {
if (json
case {
"account-id": final String newAccountId,
"access-token": final String newAccessToken,
"log": final String log,
"config": final Map<String, dynamic> wireguardConfig,
}) {
return (
log: log,
accountId: newAccountId,
accessToken: newAccessToken,
wireguardConfig: jsonEncode(wireguardConfig),
);
}
throw Exception("invalid response");
}
+48
View File
@@ -0,0 +1,48 @@
import 'package:fpdart/fpdart.dart';
import 'package:grpc/grpc.dart';
import 'package:kaer_with_panels/singbox/generated/core.pbgrpc.dart';
import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
abstract class CoreSingboxService extends CoreServiceClient
implements SingboxService {
CoreSingboxService()
: super(
ClientChannel(
'localhost',
port: 7078,
options: const ChannelOptions(
credentials: ChannelCredentials.insecure(),
),
),
);
@override
TaskEither<String, Unit> validateConfigByPath(
String path,
String tempPath,
bool debug,
) {
return TaskEither(
() async {
final response = await parseConfig(
ParseConfigRequest(tempPath: tempPath, path: path, debug: false),
);
if (response.error != "") return left(response.error);
return right(unit);
},
);
}
@override
TaskEither<String, String> generateFullConfigByPath(String path) {
return TaskEither(
() async {
final response = await generateFullConfig(
GenerateConfigRequest(path: path, debug: false),
);
if (response.error != "") return left(response.error);
return right(response.config);
},
);
}
}
+489
View File
@@ -0,0 +1,489 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
// import 'package:combine/combine.dart'; // 暂时注释掉,使用 Isolate.run 替代
import 'package:ffi/ffi.dart';
import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/core/model/directories.dart';
import 'package:kaer_with_panels/gen/singbox_generated_bindings.dart';
import 'package:kaer_with_panels/singbox/model/singbox_config_option.dart';
import 'package:kaer_with_panels/singbox/model/singbox_outbound.dart';
import 'package:kaer_with_panels/singbox/model/singbox_stats.dart';
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
import 'package:kaer_with_panels/singbox/model/warp_account.dart';
import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
import 'package:kaer_with_panels/utils/utils.dart';
import 'package:loggy/loggy.dart';
import 'package:path/path.dart' as p;
import 'package:rxdart/rxdart.dart';
import 'package:watcher/watcher.dart';
final _logger = Loggy('FFISingboxService');
class FFISingboxService with InfraLogger implements SingboxService {
static final SingboxNativeLibrary _box = _gen();
late final ValueStream<SingboxStatus> _status;
late final ReceivePort _statusReceiver;
Stream<SingboxStats>? _serviceStatsStream;
Stream<List<SingboxOutboundGroup>>? _outboundsStream;
static SingboxNativeLibrary _gen() {
String fullPath = "";
if (Platform.environment.containsKey('FLUTTER_TEST')) {
fullPath = "libcore";
}
if (Platform.isWindows) {
fullPath = p.join(fullPath, "libcore.dll");
} else if (Platform.isMacOS) {
fullPath = p.join(fullPath, "libcore.dylib");
} else {
fullPath = p.join(fullPath, "libcore.so");
}
_logger.debug('singbox native libs path: "$fullPath"');
final lib = DynamicLibrary.open(fullPath);
return SingboxNativeLibrary(lib);
}
@override
Future<void> init() async {
loggy.debug("initializing");
_statusReceiver = ReceivePort('service status receiver');
final source = _statusReceiver
.asBroadcastStream()
.map((event) => jsonDecode(event as String))
.map(SingboxStatus.fromEvent);
_status = ValueConnectableStream.seeded(
source,
const SingboxStopped(),
).autoConnect();
}
@override
TaskEither<String, Unit> setup(
Directories directories,
bool debug,
) {
final port = _statusReceiver.sendPort.nativePort;
return TaskEither(
() => Isolate.run(
() {
_box.setupOnce(NativeApi.initializeApiDLData);
final err = _box
.setup(
directories.baseDir.path.toNativeUtf8().cast(),
directories.workingDir.path.toNativeUtf8().cast(),
directories.tempDir.path.toNativeUtf8().cast(),
port,
debug ? 1 : 0,
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, Unit> validateConfigByPath(
String path,
String tempPath,
bool debug,
) {
return TaskEither(
() => Isolate.run(
() {
final err = _box
.parse(
path.toNativeUtf8().cast(),
tempPath.toNativeUtf8().cast(),
debug ? 1 : 0,
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
return TaskEither(
() => Isolate.run(
() {
final json = jsonEncode(options.toJson());
final err = _box
.changeHiddifyOptions(json.toNativeUtf8().cast())
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, String> generateFullConfigByPath(
String path,
) {
return TaskEither(
() => Isolate.run(
() {
final response = _box
.generateConfig(
path.toNativeUtf8().cast(),
)
.cast<Utf8>()
.toDartString();
if (response.startsWith("error")) {
return left(response.replaceFirst("error", ""));
}
return right(response);
},
),
);
}
@override
TaskEither<String, Unit> start(
String configPath,
String name,
bool disableMemoryLimit,
) {
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
return TaskEither(
() => Isolate.run(
() {
final err = _box
.start(
configPath.toNativeUtf8().cast(),
disableMemoryLimit ? 1 : 0,
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, Unit> stop() {
return TaskEither(
() => Isolate.run(
() {
final err = _box.stop().cast<Utf8>().toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, Unit> restart(
String configPath,
String name,
bool disableMemoryLimit,
) {
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
return TaskEither(
() => Isolate.run(
() {
final err = _box
.restart(
configPath.toNativeUtf8().cast(),
disableMemoryLimit ? 1 : 0,
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, Unit> resetTunnel() {
throw UnimplementedError(
"reset tunnel function unavailable on platform",
);
}
@override
Stream<SingboxStatus> watchStatus() => _status;
@override
Stream<SingboxStats> watchStats() {
if (_serviceStatsStream != null) return _serviceStatsStream!;
final receiver = ReceivePort('stats');
final statusStream = receiver.asBroadcastStream(
onCancel: (_) {
_logger.debug("stopping stats command client");
final err = _box.stopCommandClient(1).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
_logger.error("error stopping stats client");
}
receiver.close();
_serviceStatsStream = null;
},
).map(
(event) {
if (event case String _) {
if (event.startsWith('error:')) {
loggy.error("[service stats client] error received: $event");
throw event.replaceFirst('error:', "");
}
return SingboxStats.fromJson(
jsonDecode(event) as Map<String, dynamic>,
);
}
loggy.error("[service status client] unexpected type, msg: $event");
throw "invalid type";
},
);
final err = _box
.startCommandClient(1, receiver.sendPort.nativePort)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
loggy.error("error starting status command: $err");
throw err;
}
return _serviceStatsStream = statusStream;
}
@override
Stream<List<SingboxOutboundGroup>> watchGroups() {
final logger = newLoggy("watchGroups");
if (_outboundsStream != null) return _outboundsStream!;
final receiver = ReceivePort('groups');
final outboundsStream = receiver.asBroadcastStream(
onCancel: (_) {
logger.debug("stopping");
receiver.close();
_outboundsStream = null;
final err = _box.stopCommandClient(5).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
_logger.error("error stopping group client");
}
},
).map(
(event) {
if (event case String _) {
if (event.startsWith('error:')) {
logger.error("error received: $event");
throw event.replaceFirst('error:', "");
}
return (jsonDecode(event) as List).map((e) {
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
}).toList();
}
logger.error("unexpected type, msg: $event");
throw "invalid type";
},
);
try {
final err = _box
.startCommandClient(5, receiver.sendPort.nativePort)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
logger.error("error starting group command: $err");
throw err;
}
} catch (e) {
receiver.close();
rethrow;
}
return _outboundsStream = outboundsStream;
}
@override
Stream<List<SingboxOutboundGroup>> watchActiveGroups() {
final logger = newLoggy("[ActiveGroupsClient]");
final receiver = ReceivePort('active groups');
final outboundsStream = receiver.asBroadcastStream(
onCancel: (_) {
logger.debug("stopping");
receiver.close();
final err = _box.stopCommandClient(13).cast<Utf8>().toDartString();
if (err.isNotEmpty) {
logger.error("failed stopping: $err");
}
},
).map(
(event) {
if (event case String _) {
if (event.startsWith('error:')) {
logger.error(event);
throw event.replaceFirst('error:', "");
}
return (jsonDecode(event) as List).map((e) {
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
}).toList();
}
logger.error("unexpected type, msg: $event");
throw "invalid type";
},
);
try {
final err = _box
.startCommandClient(13, receiver.sendPort.nativePort)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
logger.error("error starting: $err");
throw err;
}
} catch (e) {
receiver.close();
rethrow;
}
return outboundsStream;
}
@override
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
return TaskEither(
() => Isolate.run(
() {
final err = _box
.selectOutbound(
groupTag.toNativeUtf8().cast(),
outboundTag.toNativeUtf8().cast(),
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, Unit> urlTest(String groupTag) {
return TaskEither(
() => Isolate.run(
() {
final err = _box
.urlTest(groupTag.toNativeUtf8().cast())
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
final _logBuffer = <String>[];
int _logFilePosition = 0;
@override
Stream<List<String>> watchLogs(String path) async* {
yield await _readLogFile(File(path));
yield* Watcher(path, pollingDelay: const Duration(seconds: 1))
.events
.asyncMap((event) async {
if (event.type == ChangeType.MODIFY) {
await _readLogFile(File(path));
}
return _logBuffer;
});
}
@override
TaskEither<String, Unit> clearLogs() {
return TaskEither(
() => Isolate.run(
() {
_logBuffer.clear();
return right(unit);
},
),
);
}
Future<List<String>> _readLogFile(File file) async {
if (_logFilePosition == 0 && file.lengthSync() == 0) return [];
final content =
await file.openRead(_logFilePosition).transform(utf8.decoder).join();
_logFilePosition = file.lengthSync();
final lines = const LineSplitter().convert(content);
if (lines.length > 300) {
lines.removeRange(0, lines.length - 300);
}
for (final line in lines) {
_logBuffer.add(line);
if (_logBuffer.length > 300) {
_logBuffer.removeAt(0);
}
}
return _logBuffer;
}
@override
TaskEither<String, WarpResponse> generateWarpConfig({
required String licenseKey,
required String previousAccountId,
required String previousAccessToken,
}) {
loggy.debug("generating warp config");
return TaskEither(
() => Isolate.run(
() {
final response = _box
.generateWarpConfig(
licenseKey.toNativeUtf8().cast(),
previousAccountId.toNativeUtf8().cast(),
previousAccessToken.toNativeUtf8().cast(),
)
.cast<Utf8>()
.toDartString();
if (response.startsWith("error:")) {
return left(response.replaceFirst('error:', ""));
}
return right(warpFromJson(jsonDecode(response)));
},
),
);
}
}
+289
View File
@@ -0,0 +1,289 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/core/model/directories.dart';
import 'package:kaer_with_panels/singbox/model/singbox_config_option.dart';
import 'package:kaer_with_panels/singbox/model/singbox_outbound.dart';
import 'package:kaer_with_panels/singbox/model/singbox_stats.dart';
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
import 'package:kaer_with_panels/singbox/model/warp_account.dart';
import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
import 'package:kaer_with_panels/utils/custom_loggers.dart';
import 'package:rxdart/rxdart.dart';
class PlatformSingboxService with InfraLogger implements SingboxService {
static const channelPrefix = "com.baer.app";
static const methodChannel = MethodChannel("$channelPrefix/method");
static const statusChannel =
EventChannel("$channelPrefix/service.status", JSONMethodCodec());
static const alertsChannel =
EventChannel("$channelPrefix/service.alerts", JSONMethodCodec());
static const statsChannel =
EventChannel("$channelPrefix/stats", JSONMethodCodec());
static const groupsChannel = EventChannel("$channelPrefix/groups");
static const activeGroupsChannel =
EventChannel("$channelPrefix/active-groups");
static const logsChannel = EventChannel("$channelPrefix/service.logs");
late final ValueStream<SingboxStatus> _status;
@override
Future<void> init() async {
loggy.debug("initializing");
final status =
statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
final alerts =
alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
_status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect();
await _status.first;
}
@override
TaskEither<String, Unit> setup(Directories directories, bool debug) {
return TaskEither(
() async {
if (!Platform.isIOS) {
return right(unit);
}
await methodChannel.invokeMethod("setup");
return right(unit);
},
);
}
@override
TaskEither<String, Unit> validateConfigByPath(
String path,
String tempPath,
bool debug,
) {
return TaskEither(
() async {
final message = await methodChannel.invokeMethod<String>(
"parse_config",
{"path": path, "tempPath": tempPath, "debug": debug},
);
if (message == null || message.isEmpty) return right(unit);
return left(message);
},
);
}
@override
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
return TaskEither(
() async {
loggy.debug("changing options");
await methodChannel.invokeMethod(
"change_hiddify_options",
jsonEncode(options.toJson()),
);
return right(unit);
},
);
}
@override
TaskEither<String, String> generateFullConfigByPath(String path) {
return TaskEither(
() async {
loggy.debug("generating full config by path");
final configJson = await methodChannel.invokeMethod<String>(
"generate_config",
{"path": path},
);
if (configJson == null || configJson.isEmpty) {
return left("null response");
}
return right(configJson);
},
);
}
@override
TaskEither<String, Unit> start(
String path,
String name,
bool disableMemoryLimit,
) {
return TaskEither(
() async {
loggy.debug("starting");
await methodChannel.invokeMethod(
"start",
{"path": path, "name": name},
);
return right(unit);
},
);
}
@override
TaskEither<String, Unit> stop() {
return TaskEither(
() async {
loggy.debug("stopping");
await methodChannel.invokeMethod("stop");
return right(unit);
},
);
}
@override
TaskEither<String, Unit> restart(
String path,
String name,
bool disableMemoryLimit,
) {
return TaskEither(
() async {
loggy.debug("restarting");
await methodChannel.invokeMethod(
"restart",
{"path": path, "name": name},
);
return right(unit);
},
);
}
@override
TaskEither<String, Unit> resetTunnel() {
return TaskEither(
() async {
// only available on iOS (and macOS later)
if (!Platform.isIOS) {
throw UnimplementedError(
"reset tunnel function unavailable on platform",
);
}
loggy.debug("resetting tunnel");
await methodChannel.invokeMethod("reset");
return right(unit);
},
);
}
@override
Stream<List<SingboxOutboundGroup>> watchGroups() {
loggy.debug("watching groups");
return groupsChannel.receiveBroadcastStream().map(
(event) {
if (event case String _) {
return (jsonDecode(event) as List).map((e) {
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
}).toList();
}
loggy.error("[group client] unexpected type, msg: $event");
throw "invalid type";
},
);
}
@override
Stream<List<SingboxOutboundGroup>> watchActiveGroups() {
loggy.debug("watching active groups");
return activeGroupsChannel.receiveBroadcastStream().map(
(event) {
if (event case String _) {
return (jsonDecode(event) as List).map((e) {
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
}).toList();
}
loggy.error("[active group client] unexpected type, msg: $event");
throw "invalid type";
},
);
}
@override
Stream<SingboxStatus> watchStatus() => _status;
@override
Stream<SingboxStats> watchStats() {
loggy.debug("watching stats");
return statsChannel.receiveBroadcastStream().map(
(event) {
if (event case Map<String, dynamic> _) {
return SingboxStats.fromJson(event);
}
loggy.error(
"[stats client] unexpected type(${event.runtimeType}), msg: $event",
);
throw "invalid type";
},
);
}
@override
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
return TaskEither(
() async {
loggy.debug("selecting outbound");
await methodChannel.invokeMethod(
"select_outbound",
{"groupTag": groupTag, "outboundTag": outboundTag},
);
return right(unit);
},
);
}
@override
TaskEither<String, Unit> urlTest(String groupTag) {
return TaskEither(
() async {
await methodChannel.invokeMethod(
"url_test",
{"groupTag": groupTag},
);
return right(unit);
},
);
}
@override
Stream<List<String>> watchLogs(String path) async* {
yield* logsChannel
.receiveBroadcastStream()
.map((event) => (event as List).map((e) => e as String).toList());
}
@override
TaskEither<String, Unit> clearLogs() {
return TaskEither(
() async {
await methodChannel.invokeMethod("clear_logs");
return right(unit);
},
);
}
@override
TaskEither<String, WarpResponse> generateWarpConfig({
required String licenseKey,
required String previousAccountId,
required String previousAccessToken,
}) {
return TaskEither(
() async {
loggy.debug("generating warp config");
final warpConfig = await methodChannel.invokeMethod(
"generate_warp_config",
{
"license-key": licenseKey,
"previous-account-id": previousAccountId,
"previous-access-token": previousAccessToken,
},
);
return right(warpFromJson(jsonDecode(warpConfig as String)));
},
);
}
}
+96
View File
@@ -0,0 +1,96 @@
import 'dart:io';
import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/core/model/directories.dart';
import 'package:kaer_with_panels/singbox/model/singbox_config_option.dart';
import 'package:kaer_with_panels/singbox/model/singbox_outbound.dart';
import 'package:kaer_with_panels/singbox/model/singbox_stats.dart';
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
import 'package:kaer_with_panels/singbox/model/warp_account.dart';
import 'package:kaer_with_panels/singbox/service/ffi_singbox_service.dart';
import 'package:kaer_with_panels/singbox/service/platform_singbox_service.dart';
abstract interface class SingboxService {
factory SingboxService() {
if (Platform.isAndroid || Platform.isIOS) {
return PlatformSingboxService();
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
return FFISingboxService();
}
throw Exception("unsupported platform");
}
Future<void> init();
/// setup directories and other initial platform services
TaskEither<String, Unit> setup(
Directories directories,
bool debug,
);
/// validates config by path and save it
///
/// [path] is used to save validated config
/// [tempPath] includes base config, possibly invalid
/// [debug] indicates if debug mode (avoid in prod)
TaskEither<String, Unit> validateConfigByPath(
String path,
String tempPath,
bool debug,
);
TaskEither<String, Unit> changeOptions(SingboxConfigOption options);
/// generates full sing-box configuration
///
/// [path] is the path to the base config file
/// returns full patched json config file as string
TaskEither<String, String> generateFullConfigByPath(String path);
/// start sing-box service
///
/// [path] is the path to the base config file (to be patched by previously set [SingboxConfigOption])
/// [name] is the name of the active profile (not unique, used for presentation in platform specific ui)
/// [disableMemoryLimit] is used to disable service memory limit (mostly used in mobile platforms i.e. iOS)
TaskEither<String, Unit> start(
String path,
String name,
bool disableMemoryLimit,
);
TaskEither<String, Unit> stop();
/// similar to [start], but uses platform dependent behavior to restart the service
TaskEither<String, Unit> restart(
String path,
String name,
bool disableMemoryLimit,
);
TaskEither<String, Unit> resetTunnel();
Stream<List<SingboxOutboundGroup>> watchGroups();
Stream<List<SingboxOutboundGroup>> watchActiveGroups();
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag);
TaskEither<String, Unit> urlTest(String groupTag);
/// watch status of sing-box service (started, starting, etc.)
Stream<SingboxStatus> watchStatus();
/// watch stats of sing-box service (uplink, downlink, etc.)
Stream<SingboxStats> watchStats();
Stream<List<String>> watchLogs(String path);
TaskEither<String, Unit> clearLogs();
TaskEither<String, WarpResponse> generateWarpConfig({
required String licenseKey,
required String previousAccountId,
required String previousAccessToken,
});
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'singbox_service_provider.g.dart';
@Riverpod(keepAlive: true)
SingboxService singboxService(SingboxServiceRef ref) {
return SingboxService();
}