new: add custom url-test for each repository

This commit is contained in:
hiddify-com
2024-07-29 13:11:51 +02:00
parent ccbc416c49
commit b9afe586bf
14 changed files with 417 additions and 157 deletions

View File

@@ -39,7 +39,7 @@ DISTRIBUTOR_ARGS=--skip-clean --build-target $(TARGET) --build-dart-define sentr
get: get:
flutter pub get flutter pub get
gen: gen:

View File

@@ -21,7 +21,7 @@ class AppDatabase extends _$AppDatabase with InfraLogger {
AppDatabase.connect() : super(openConnection()); AppDatabase.connect() : super(openConnection());
@override @override
int get schemaVersion => 3; int get schemaVersion => 4;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@@ -48,6 +48,9 @@ class AppDatabase extends _$AppDatabase with InfraLogger {
await m.createTable(schema.geoAssetEntries); await m.createTable(schema.geoAssetEntries);
await _prePopulateGeoAssets(); await _prePopulateGeoAssets();
}, },
from3To4: (m, schema) async {
await m.addColumn(profileEntries, profileEntries.testUrl);
},
), ),
beforeOpen: (details) async { beforeOpen: (details) async {
if (kDebugMode) { if (kDebugMode) {

View File

@@ -39,78 +39,38 @@ final class _S2 extends i0.VersionedSchema {
class Shape0 extends i0.VersionedTable { class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased(); Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id => i1.GeneratedColumn<String> get id => columnsByName['id']! as i1.GeneratedColumn<String>;
columnsByName['id']! as i1.GeneratedColumn<String>; i1.GeneratedColumn<String> get type => columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type => i1.GeneratedColumn<bool> get active => columnsByName['active']! as i1.GeneratedColumn<bool>;
columnsByName['type']! as i1.GeneratedColumn<String>; i1.GeneratedColumn<String> get name => columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get active => i1.GeneratedColumn<String> get url => columnsByName['url']! as i1.GeneratedColumn<String>;
columnsByName['active']! as i1.GeneratedColumn<bool>; i1.GeneratedColumn<DateTime> get lastUpdate => columnsByName['last_update']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get name => i1.GeneratedColumn<int> get updateInterval => columnsByName['update_interval']! as i1.GeneratedColumn<int>;
columnsByName['name']! as i1.GeneratedColumn<String>; i1.GeneratedColumn<int> get upload => columnsByName['upload']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get url => i1.GeneratedColumn<int> get download => columnsByName['download']! as i1.GeneratedColumn<int>;
columnsByName['url']! as i1.GeneratedColumn<String>; i1.GeneratedColumn<int> get total => columnsByName['total']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get lastUpdate => i1.GeneratedColumn<DateTime> get expire => columnsByName['expire']! as i1.GeneratedColumn<DateTime>;
columnsByName['last_update']! as i1.GeneratedColumn<DateTime>; i1.GeneratedColumn<String> get webPageUrl => columnsByName['web_page_url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get updateInterval => i1.GeneratedColumn<String> get supportUrl => columnsByName['support_url']! as i1.GeneratedColumn<String>;
columnsByName['update_interval']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get upload =>
columnsByName['upload']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get download =>
columnsByName['download']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get total =>
columnsByName['total']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get expire =>
columnsByName['expire']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get webPageUrl =>
columnsByName['web_page_url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get supportUrl =>
columnsByName['support_url']! as i1.GeneratedColumn<String>;
} }
i1.GeneratedColumn<String> _column_0(String aliasedName) => i1.GeneratedColumn<String> _column_0(String aliasedName) => i1.GeneratedColumn<String>('id', aliasedName, false, type: i1.DriftSqlType.string);
i1.GeneratedColumn<String>('id', aliasedName, false, i1.GeneratedColumn<String> _column_1(String aliasedName) => i1.GeneratedColumn<String>('type', aliasedName, false, type: i1.DriftSqlType.string);
type: i1.DriftSqlType.string); i1.GeneratedColumn<bool> _column_2(String aliasedName) => i1.GeneratedColumn<bool>('active', aliasedName, false, type: i1.DriftSqlType.bool, defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))'));
i1.GeneratedColumn<String> _column_1(String aliasedName) => i1.GeneratedColumn<String> _column_3(String aliasedName) => i1.GeneratedColumn<String>('name', aliasedName, false,
i1.GeneratedColumn<String>('type', aliasedName, false, additionalChecks: i1.GeneratedColumn.checkTextLength(
type: i1.DriftSqlType.string); minTextLength: 1,
i1.GeneratedColumn<bool> _column_2(String aliasedName) => ),
i1.GeneratedColumn<bool>('active', aliasedName, false, type: i1.DriftSqlType.string);
type: i1.DriftSqlType.bool, i1.GeneratedColumn<String> _column_4(String aliasedName) => i1.GeneratedColumn<String>('url', aliasedName, true, type: i1.DriftSqlType.string);
defaultConstraints: i1.GeneratedColumn.constraintIsAlways( i1.GeneratedColumn<DateTime> _column_5(String aliasedName) => i1.GeneratedColumn<DateTime>('last_update', aliasedName, false, type: i1.DriftSqlType.dateTime);
'CHECK ("active" IN (0, 1))')); i1.GeneratedColumn<int> _column_6(String aliasedName) => i1.GeneratedColumn<int>('update_interval', aliasedName, true, type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_3(String aliasedName) => i1.GeneratedColumn<int> _column_7(String aliasedName) => i1.GeneratedColumn<int>('upload', aliasedName, true, type: i1.DriftSqlType.int);
i1.GeneratedColumn<String>('name', aliasedName, false, i1.GeneratedColumn<int> _column_8(String aliasedName) => i1.GeneratedColumn<int>('download', aliasedName, true, type: i1.DriftSqlType.int);
additionalChecks: i1.GeneratedColumn.checkTextLength( i1.GeneratedColumn<int> _column_9(String aliasedName) => i1.GeneratedColumn<int>('total', aliasedName, true, type: i1.DriftSqlType.int);
minTextLength: 1, i1.GeneratedColumn<DateTime> _column_10(String aliasedName) => i1.GeneratedColumn<DateTime>('expire', aliasedName, true, type: i1.DriftSqlType.dateTime);
), i1.GeneratedColumn<String> _column_11(String aliasedName) => i1.GeneratedColumn<String>('web_page_url', aliasedName, true, type: i1.DriftSqlType.string);
type: i1.DriftSqlType.string); i1.GeneratedColumn<String> _column_12(String aliasedName) => i1.GeneratedColumn<String>('support_url', aliasedName, true, type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
i1.GeneratedColumn<String>('url', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) =>
i1.GeneratedColumn<DateTime>('last_update', aliasedName, false,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
i1.GeneratedColumn<int>('update_interval', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
i1.GeneratedColumn<int>('upload', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_8(String aliasedName) =>
i1.GeneratedColumn<int>('download', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_9(String aliasedName) =>
i1.GeneratedColumn<int>('total', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
i1.GeneratedColumn<DateTime>('expire', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_11(String aliasedName) =>
i1.GeneratedColumn<String>('web_page_url', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
i1.GeneratedColumn<String>('support_url', aliasedName, true,
type: i1.DriftSqlType.string);
final class _S3 extends i0.VersionedSchema { final class _S3 extends i0.VersionedSchema {
_S3({required super.database}) : super(version: 3); _S3({required super.database}) : super(version: 3);
@@ -170,37 +130,26 @@ final class _S3 extends i0.VersionedSchema {
class Shape1 extends i0.VersionedTable { class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased(); Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id => i1.GeneratedColumn<String> get id => columnsByName['id']! as i1.GeneratedColumn<String>;
columnsByName['id']! as i1.GeneratedColumn<String>; i1.GeneratedColumn<String> get type => columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type => i1.GeneratedColumn<bool> get active => columnsByName['active']! as i1.GeneratedColumn<bool>;
columnsByName['type']! as i1.GeneratedColumn<String>; i1.GeneratedColumn<String> get name => columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get active => i1.GeneratedColumn<String> get providerName => columnsByName['provider_name']! as i1.GeneratedColumn<String>;
columnsByName['active']! as i1.GeneratedColumn<bool>; i1.GeneratedColumn<String> get version => columnsByName['version']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name => i1.GeneratedColumn<DateTime> get lastCheck => columnsByName['last_check']! as i1.GeneratedColumn<DateTime>;
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get providerName =>
columnsByName['provider_name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get version =>
columnsByName['version']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get lastCheck =>
columnsByName['last_check']! as i1.GeneratedColumn<DateTime>;
} }
i1.GeneratedColumn<String> _column_13(String aliasedName) => i1.GeneratedColumn<String> _column_13(String aliasedName) => i1.GeneratedColumn<String>('provider_name', aliasedName, false,
i1.GeneratedColumn<String>('provider_name', aliasedName, false, additionalChecks: i1.GeneratedColumn.checkTextLength(
additionalChecks: i1.GeneratedColumn.checkTextLength( minTextLength: 1,
minTextLength: 1, ),
), type: i1.DriftSqlType.string);
type: i1.DriftSqlType.string); i1.GeneratedColumn<String> _column_14(String aliasedName) => i1.GeneratedColumn<String>('version', aliasedName, true, type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_14(String aliasedName) => i1.GeneratedColumn<DateTime> _column_15(String aliasedName) => i1.GeneratedColumn<DateTime>('last_check', aliasedName, true, type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String>('version', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_15(String aliasedName) =>
i1.GeneratedColumn<DateTime>('last_check', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2, required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3, required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
required Future<void> Function(i1.Migrator m, _S3 schema) from3To4,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@@ -214,6 +163,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema); await from2To3(migrator, schema);
return 3; return 3;
case 3:
final schema = _S3(database: database);
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@@ -223,9 +177,11 @@ i0.MigrationStepWithVersion migrationSteps({
i1.OnUpgrade stepByStep({ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2, required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3, required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
required Future<void> Function(i1.Migrator m, _S3 schema) from3To4,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
from2To3: from2To3, from2To3: from2To3,
from3To4: from3To4,
)); ));

View File

@@ -0,0 +1,296 @@
{
"_meta": {
"description": "This file contains a serialized version of schema entities for drift.",
"version": "1.1.0"
},
"options": {
"store_date_time_values_as_text": true
},
"entities": [
{
"id": 0,
"references": [],
"type": "table",
"data": {
"name": "profile_entries",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"getter_name": "id",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "type",
"getter_name": "type",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "const EnumNameConverter<ProfileType>(ProfileType.values)",
"dart_type_name": "ProfileType"
}
},
{
"name": "active",
"getter_name": "active",
"moor_type": "bool",
"nullable": false,
"customConstraints": null,
"defaultConstraints": "CHECK (\"active\" IN (0, 1))",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "name",
"getter_name": "name",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [
{
"allowed-lengths": {
"min": 1,
"max": null
}
}
]
},
{
"name": "url",
"getter_name": "url",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_update",
"getter_name": "lastUpdate",
"moor_type": "dateTime",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "update_interval",
"getter_name": "updateInterval",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "DurationTypeConverter()",
"dart_type_name": "Duration"
}
},
{
"name": "upload",
"getter_name": "upload",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "download",
"getter_name": "download",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "total",
"getter_name": "total",
"moor_type": "int",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "expire",
"getter_name": "expire",
"moor_type": "dateTime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "web_page_url",
"getter_name": "webPageUrl",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "support_url",
"getter_name": "supportUrl",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "test_url",
"getter_name": "testUrl",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"without_rowid": false,
"constraints": [],
"explicit_pk": [
"id"
]
}
},
{
"id": 1,
"references": [],
"type": "table",
"data": {
"name": "geo_asset_entries",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"getter_name": "id",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "type",
"getter_name": "type",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "const EnumNameConverter<GeoAssetType>(GeoAssetType.values)",
"dart_type_name": "GeoAssetType"
}
},
{
"name": "active",
"getter_name": "active",
"moor_type": "bool",
"nullable": false,
"customConstraints": null,
"defaultConstraints": "CHECK (\"active\" IN (0, 1))",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "name",
"getter_name": "name",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [
{
"allowed-lengths": {
"min": 1,
"max": null
}
}
]
},
{
"name": "provider_name",
"getter_name": "providerName",
"moor_type": "string",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [
{
"allowed-lengths": {
"min": 1,
"max": null
}
}
]
},
{
"name": "version",
"getter_name": "version",
"moor_type": "string",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_check",
"getter_name": "lastCheck",
"moor_type": "dateTime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"without_rowid": false,
"constraints": [],
"explicit_pk": [
"id"
],
"unique_keys": [
[
"name",
"provider_name"
]
]
}
}
]
}

View File

@@ -11,14 +11,14 @@ class ProfileEntries extends Table {
TextColumn get name => text().withLength(min: 1)(); TextColumn get name => text().withLength(min: 1)();
TextColumn get url => text().nullable()(); TextColumn get url => text().nullable()();
DateTimeColumn get lastUpdate => dateTime()(); DateTimeColumn get lastUpdate => dateTime()();
IntColumn get updateInterval => IntColumn get updateInterval => integer().nullable().map(DurationTypeConverter())();
integer().nullable().map(DurationTypeConverter())();
IntColumn get upload => integer().nullable()(); IntColumn get upload => integer().nullable()();
IntColumn get download => integer().nullable()(); IntColumn get download => integer().nullable()();
IntColumn get total => integer().nullable()(); IntColumn get total => integer().nullable()();
DateTimeColumn get expire => dateTime().nullable()(); DateTimeColumn get expire => dateTime().nullable()();
TextColumn get webPageUrl => text().nullable()(); TextColumn get webPageUrl => text().nullable()();
TextColumn get supportUrl => text().nullable()(); TextColumn get supportUrl => text().nullable()();
TextColumn get testUrl => text().nullable()();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};

View File

@@ -22,12 +22,14 @@ abstract interface class ConnectionRepository {
String fileName, String fileName,
String profileName, String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
String? testUrl,
); );
TaskEither<ConnectionFailure, Unit> disconnect(); TaskEither<ConnectionFailure, Unit> disconnect();
TaskEither<ConnectionFailure, Unit> reconnect( TaskEither<ConnectionFailure, Unit> reconnect(
String fileName, String fileName,
String profileName, String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
String? testUrl,
); );
} }
@@ -101,11 +103,16 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
@visibleForTesting @visibleForTesting
TaskEither<ConnectionFailure, Unit> applyConfigOption( TaskEither<ConnectionFailure, Unit> applyConfigOption(
SingboxConfigOption options, SingboxConfigOption options,
String? testUrl,
) { ) {
return exceptionHandler( return exceptionHandler(
() { () {
_configOptionsSnapshot = options; _configOptionsSnapshot = options;
return singbox.changeOptions(options).mapLeft(InvalidConfigOption.new).run(); var newOptions = options;
if (testUrl != null) {
newOptions = options.copyWith(connectionTestUrl: testUrl);
}
return singbox.changeOptions(newOptions).mapLeft(InvalidConfigOption.new).run();
}, },
UnexpectedConnectionFailure.new, UnexpectedConnectionFailure.new,
); );
@@ -138,10 +145,11 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
String fileName, String fileName,
String profileName, String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
String? testUrl,
) { ) {
return TaskEither<ConnectionFailure, Unit>.Do( return TaskEither<ConnectionFailure, Unit>.Do(
($) async { ($) async {
final options = await $(getConfigOption()); var options = await $(getConfigOption());
loggy.info( loggy.info(
"config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}",
); );
@@ -159,7 +167,7 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
}), }),
); );
await $(setup()); await $(setup());
await $(applyConfigOption(options)); await $(applyConfigOption(options, testUrl));
return await $( return await $(
singbox singbox
.start( .start(
@@ -203,23 +211,26 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con
String fileName, String fileName,
String profileName, String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
String? testUrl,
) { ) {
return exceptionHandler( return TaskEither<ConnectionFailure, Unit>.Do(
() async { ($) async {
return getConfigOption() var options = await $(getConfigOption());
.flatMap((options) => applyConfigOption(options)) loggy.info(
.andThen( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}",
() => singbox );
.restart(
profilePathResolver.file(fileName).path, await $(applyConfigOption(options, testUrl));
profileName, return await $(
disableMemoryLimit, singbox
) .restart(
.mapLeft(UnexpectedConnectionFailure.new), profilePathResolver.file(fileName).path,
) profileName,
.run(); disableMemoryLimit,
)
.mapLeft(UnexpectedConnectionFailure.new),
);
}, },
UnexpectedConnectionFailure.new, ).handleExceptions(UnexpectedConnectionFailure.new);
);
} }
} }

View File

@@ -103,6 +103,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
profile.id, profile.id,
profile.name, profile.name,
ref.read(Preferences.disableMemoryLimit), ref.read(Preferences.disableMemoryLimit),
profile.testUrl,
) )
.mapLeft((err) { .mapLeft((err) {
loggy.warning("error reconnecting", err); loggy.warning("error reconnecting", err);
@@ -133,6 +134,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
activeProfile.id, activeProfile.id,
activeProfile.name, activeProfile.name,
ref.read(Preferences.disableMemoryLimit), ref.read(Preferences.disableMemoryLimit),
activeProfile.testUrl,
) )
.mapLeft((err) async { .mapLeft((err) async {
loggy.warning("error connecting", err); loggy.warning("error connecting", err);

View File

@@ -5,8 +5,7 @@ import 'package:hiddify/features/profile/model/profile_entity.dart';
extension ProfileEntityMapper on ProfileEntity { extension ProfileEntityMapper on ProfileEntity {
ProfileEntriesCompanion toEntry() { ProfileEntriesCompanion toEntry() {
return switch (this) { return switch (this) {
RemoteProfileEntity(:final url, :final options, :final subInfo) => RemoteProfileEntity(:final url, :final options, :final subInfo) => ProfileEntriesCompanion.insert(
ProfileEntriesCompanion.insert(
id: id, id: id,
type: ProfileType.remote, type: ProfileType.remote,
active: active, active: active,
@@ -20,6 +19,7 @@ extension ProfileEntityMapper on ProfileEntity {
expire: Value(subInfo?.expire), expire: Value(subInfo?.expire),
webPageUrl: Value(subInfo?.webPageUrl), webPageUrl: Value(subInfo?.webPageUrl),
supportUrl: Value(subInfo?.supportUrl), supportUrl: Value(subInfo?.supportUrl),
testUrl: Value(testUrl),
), ),
LocalProfileEntity() => ProfileEntriesCompanion.insert( LocalProfileEntity() => ProfileEntriesCompanion.insert(
id: id, id: id,
@@ -41,6 +41,7 @@ extension RemoteProfileEntityMapper on RemoteProfileEntity {
expire: Value(subInfo?.expire), expire: Value(subInfo?.expire),
webPageUrl: Value(subInfo?.webPageUrl), webPageUrl: Value(subInfo?.webPageUrl),
supportUrl: Value(subInfo?.supportUrl), supportUrl: Value(subInfo?.supportUrl),
testUrl: Value(testUrl),
); );
} }
} }
@@ -73,12 +74,14 @@ extension ProfileEntryMapper on ProfileEntry {
lastUpdate: lastUpdate, lastUpdate: lastUpdate,
options: options, options: options,
subInfo: subInfo, subInfo: subInfo,
testUrl: testUrl,
), ),
ProfileType.local => LocalProfileEntity( ProfileType.local => LocalProfileEntity(
id: id, id: id,
active: active, active: active,
name: name, name: name,
lastUpdate: lastUpdate, lastUpdate: lastUpdate,
testUrl: testUrl,
), ),
}; };
} }

View File

@@ -24,14 +24,12 @@ abstract class ProfileParser {
var name = ''; var name = '';
if (headers['profile-title'] case [final titleHeader]) { if (headers['profile-title'] case [final titleHeader]) {
if (titleHeader.startsWith("base64:")) { if (titleHeader.startsWith("base64:")) {
name = name = utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", "")));
utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", "")));
} else { } else {
name = titleHeader.trim(); name = titleHeader.trim();
} }
} }
if (headers['content-disposition'] case [final contentDispositionHeader] if (headers['content-disposition'] case [final contentDispositionHeader] when name.isEmpty) {
when name.isEmpty) {
final regExp = RegExp('filename="([^"]*)"'); final regExp = RegExp('filename="([^"]*)"');
final match = regExp.firstMatch(contentDispositionHeader); final match = regExp.firstMatch(contentDispositionHeader);
if (match != null && match.groupCount >= 1) { if (match != null && match.groupCount >= 1) {
@@ -52,19 +50,20 @@ abstract class ProfileParser {
final updateInterval = Duration(hours: int.parse(updateIntervalStr)); final updateInterval = Duration(hours: int.parse(updateIntervalStr));
options = ProfileOptions(updateInterval: updateInterval); options = ProfileOptions(updateInterval: updateInterval);
} }
String? testUrl;
if (headers['test-url'] case [final testUrl_] when isUrl(testUrl_)) {
testUrl = testUrl_;
}
SubscriptionInfo? subInfo; SubscriptionInfo? subInfo;
if (headers['subscription-userinfo'] case [final subInfoStr]) { if (headers['subscription-userinfo'] case [final subInfoStr]) {
subInfo = parseSubscriptionInfo(subInfoStr); subInfo = parseSubscriptionInfo(subInfoStr);
} }
if (subInfo != null) { if (subInfo != null) {
if (headers['profile-web-page-url'] case [final profileWebPageUrl] if (headers['profile-web-page-url'] case [final profileWebPageUrl] when isUrl(profileWebPageUrl)) {
when isUrl(profileWebPageUrl)) {
subInfo = subInfo.copyWith(webPageUrl: profileWebPageUrl); subInfo = subInfo.copyWith(webPageUrl: profileWebPageUrl);
} }
if (headers['support-url'] case [final profileSupportUrl] if (headers['support-url'] case [final profileSupportUrl] when isUrl(profileSupportUrl)) {
when isUrl(profileSupportUrl)) {
subInfo = subInfo.copyWith(supportUrl: profileSupportUrl); subInfo = subInfo.copyWith(supportUrl: profileSupportUrl);
} }
} }
@@ -77,23 +76,16 @@ abstract class ProfileParser {
lastUpdate: DateTime.now(), lastUpdate: DateTime.now(),
options: options, options: options,
subInfo: subInfo, subInfo: subInfo,
testUrl: testUrl,
); );
} }
static SubscriptionInfo? parseSubscriptionInfo(String subInfoStr) { static SubscriptionInfo? parseSubscriptionInfo(String subInfoStr) {
final values = subInfoStr.split(';'); final values = subInfoStr.split(';');
final map = { final map = {
for (final v in values) for (final v in values) v.split('=').first.trim(): num.tryParse(v.split('=').second.trim())?.toInt(),
v.split('=').first.trim():
num.tryParse(v.split('=').second.trim())?.toInt(),
}; };
if (map if (map case {"upload": final upload?, "download": final download?, "total": var total, "expire": var expire}) {
case {
"upload": final upload?,
"download": final download?,
"total": var total,
"expire": var expire
}) {
total = (total == null || total == 0) ? infiniteTrafficThreshold : total; total = (total == null || total == 0) ? infiniteTrafficThreshold : total;
expire = (expire == null || expire == 0) ? infiniteTimeThreshold : expire; expire = (expire == null || expire == 0) ? infiniteTimeThreshold : expire;
return SubscriptionInfo( return SubscriptionInfo(

View File

@@ -282,6 +282,7 @@ class ProfileRepositoryImpl with ExceptionHandler, InfraLogger implements Profil
? profilePatch.copyWith( ? profilePatch.copyWith(
name: Value(baseProfile.name), name: Value(baseProfile.name),
url: Value(baseProfile.url), url: Value(baseProfile.url),
testUrl: Value(baseProfile.testUrl),
updateInterval: Value(baseProfile.options?.updateInterval), updateInterval: Value(baseProfile.options?.updateInterval),
) )
: profilePatch, : profilePatch,
@@ -349,6 +350,7 @@ class ProfileRepositoryImpl with ExceptionHandler, InfraLogger implements Profil
'profile-update-interval', 'profile-update-interval',
'support-url', 'support-url',
'profile-web-page-url', 'profile-web-page-url',
'test-url',
]; ];
@visibleForTesting @visibleForTesting

View File

@@ -15,6 +15,7 @@ sealed class ProfileEntity with _$ProfileEntity {
required String name, required String name,
required String url, required String url,
required DateTime lastUpdate, required DateTime lastUpdate,
String? testUrl,
ProfileOptions? options, ProfileOptions? options,
SubscriptionInfo? subInfo, SubscriptionInfo? subInfo,
}) = RemoteProfileEntity; }) = RemoteProfileEntity;
@@ -24,6 +25,7 @@ sealed class ProfileEntity with _$ProfileEntity {
required bool active, required bool active,
required String name, required String name,
required DateTime lastUpdate, required DateTime lastUpdate,
String? testUrl,
}) = LocalProfileEntity; }) = LocalProfileEntity;
} }

View File

@@ -17,15 +17,11 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
static const channelPrefix = "com.hiddify.app"; static const channelPrefix = "com.hiddify.app";
static const methodChannel = MethodChannel("$channelPrefix/method"); static const methodChannel = MethodChannel("$channelPrefix/method");
static const statusChannel = static const statusChannel = EventChannel("$channelPrefix/service.status", JSONMethodCodec());
EventChannel("$channelPrefix/service.status", JSONMethodCodec()); static const alertsChannel = EventChannel("$channelPrefix/service.alerts", JSONMethodCodec());
static const alertsChannel = static const statsChannel = EventChannel("$channelPrefix/stats", JSONMethodCodec());
EventChannel("$channelPrefix/service.alerts", JSONMethodCodec());
static const statsChannel =
EventChannel("$channelPrefix/stats", JSONMethodCodec());
static const groupsChannel = EventChannel("$channelPrefix/groups"); static const groupsChannel = EventChannel("$channelPrefix/groups");
static const activeGroupsChannel = static const activeGroupsChannel = EventChannel("$channelPrefix/active-groups");
EventChannel("$channelPrefix/active-groups");
static const logsChannel = EventChannel("$channelPrefix/service.logs"); static const logsChannel = EventChannel("$channelPrefix/service.logs");
late final ValueStream<SingboxStatus> _status; late final ValueStream<SingboxStatus> _status;
@@ -33,10 +29,8 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
@override @override
Future<void> init() async { Future<void> init() async {
loggy.debug("initializing"); loggy.debug("initializing");
final status = final status = statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); final alerts = alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
final alerts =
alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent);
_status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); _status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect();
await _status.first; await _status.first;
@@ -250,9 +244,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
@override @override
Stream<List<String>> watchLogs(String path) async* { Stream<List<String>> watchLogs(String path) async* {
yield* logsChannel yield* logsChannel.receiveBroadcastStream().map((event) => (event as List).map((e) => e as String).toList());
.receiveBroadcastStream()
.map((event) => (event as List).map((e) => e as String).toList());
} }
@override @override

View File

@@ -1,6 +1,7 @@
/// https://gist.github.com/dperini/729294 /// https://gist.github.com/dperini/729294
final _urlRegex = RegExp( final _urlRegex = RegExp(
r"^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$", // r"^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$",
r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+',
); );
/// https://stackoverflow.com/a/12968117 /// https://stackoverflow.com/a/12968117

Submodule libcore updated: e6db5f2348...77fe588eae